Lesson 04-Three.js Basic Syntax and Usage

Three.js Basics

Introduction to Three.js

Three.js Overview
Three.js is a JavaScript library based on WebGL, designed for creating and displaying 3D graphics in web browsers. WebGL is a low-level API that provides hardware-accelerated 3D rendering in browsers but is complex to use directly. Three.js abstracts this complexity, enabling developers to create sophisticated 3D scenes, objects, animations, and interactions more easily. It includes components for scene management, geometry, materials, lights, cameras, and animation systems, while handling compatibility to ensure 3D graphics work across modern browsers.

Official Documentation and Resources

Three.js Official Website and Documentation

GitHub Repository

  • Three.js GitHub: Access the source code, view commit history, report issues, or contribute code.

Examples and Tutorials

  • Three.js Examples: A collection of official example code ranging from basic to advanced scenarios, ideal for learning.
  • Three.js Fundamentals: A popular unofficial resource with tutorials and in-depth explanations for learners of all levels.

Community and Support

Features of Three.js

  • Ease of Use: Provides high-level APIs, allowing developers to create 3D applications without deep knowledge of WebGL.
  • Broad Compatibility: Supports multiple browsers, including desktop and mobile devices, reducing cross-platform development effort.
  • Rich Features: Includes built-in 3D objects, geometries, textures, lighting, camera types, animations, and physics support.
  • Active Community: Offers numerous examples, plugins, tutorials, and a vibrant developer community for learning and problem-solving.
  • Performance Optimization: Optimizes common operations, such as batch rendering, to enhance performance.

Environment Setup

HTML File

Create an HTML file to host your JavaScript code and WebGL canvas. Include the Three.js library in the <head> section, typically three.js or the minified three.min.js.

<!DOCTYPE html>
<html>
<head>
    <title>My First Three.js Scene</title>
    <script src="path/to/three.js"></script>
</head>
<body>
    <canvas id="canvas"></canvas>
    <script src="main.js"></script>
</body>
</html>

CSS

Set basic CSS to ensure the canvas fills the viewport.

body { margin: 0; }
canvas { display: block; }

JavaScript File (main.js)

In the JavaScript file, create a scene, camera, renderer, and add a simple 3D object, such as a cube.

var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);

camera.position.z = 5;

function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}
animate();

This code creates a green cube positioned in front of the camera and renders it continuously. requestAnimationFrame(animate) drives the animation loop, re-rendering the scene each frame.

Scene

In Three.js, the THREE.Scene class is the foundation of a 3D scene, acting as a container for all objects to be rendered in 3D space, including geometries, lights, cameras, and helpers.

Basic Structure

Creating a scene is straightforward with new THREE.Scene(). The constructor takes no parameters and initializes internal properties, such as the children array, to store all objects in the scene.

const scene = new THREE.Scene();

Properties

THREE.Scene inherits from THREE.Object3D, so it has all Object3D properties. Key properties include:

  • children: An array containing all child objects in the scene, such as geometries, cameras, and lights.
  • fog: Optional fog effect, an instance of THREE.Fog or THREE.FogExp2, affecting visibility of distant objects.
  • background: Can be set to a color, texture, or gradient to define the scene’s background.

Methods

THREE.Scene inherits most THREE.Object3D methods, including:

  • add(object): Adds a 3D object to the scene’s children list.
  • remove(object): Removes a 3D object from the scene.
  • getObjectById(id): Retrieves an object by its ID.
  • getObjectByName(name): Retrieves an object by its name.
  • raycast(raycaster, intersects): Performs raycasting for detecting mouse or touch interactions with scene objects.
  • traverse(callback): Iterates through all children in the scene graph, executing a callback for each object.
// Import Three.js library
import * as THREE from 'three';

// Create scene
const scene = new THREE.Scene();

// Create camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 10);

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

// Load environment texture
const environmentTextureLoader = new THREE.TextureLoader();
const environmentMap = environmentTextureLoader.load('path/to/environment_map.jpg');

// Create cube
const cubeGeo = new THREE.BoxGeometry(2, 2, 2);
const cubeMat = new THREE.MeshPhongMaterial({
    color: 0x44aa88,
    envMap: environmentMap,
});
const cube = new THREE.Mesh(cubeGeo, cubeMat);
cube.rotation.x = -0.2;
scene.add(cube);

// Create sphere
const sphereGeo = new THREE.SphereGeometry(1, 32, 32);
const sphereMat = new THREE.MeshPhongMaterial({
    color: 0xff8844,
    envMap: environmentMap,
});
const sphere = new THREE.Mesh(sphereGeo, sphereMat);
sphere.position.y = 3;
scene.add(sphere);

// Add lighting
const ambientLight = new THREE.AmbientLight(0x404040); // Soft white light
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(1, 1, 1).normalize();
scene.add(directionalLight);

const pointLight = new THREE.PointLight(0xff0000, 1, 100);
pointLight.position.set(-10, 20, 15);
scene.add(pointLight);

// Add fog effect
scene.fog = new THREE.FogExp2(0x000000, 0.002); // Black fog with low density

// Render loop
function animate() {
    requestAnimationFrame(animate);
    cube.rotation.y += 0.01;
    sphere.rotation.y -= 0.01;
    renderer.render(scene, camera);
}
animate();

Code Analysis

  1. Creates a scene and camera, positioning the camera at an observation point.
  2. Sets up a WebGL renderer and appends it to the HTML document.
  3. Loads an environment texture and applies it to the cube and sphere materials, reflecting the environment on their surfaces.
  4. Creates two 3D geometries (cube and sphere) with distinct materials and positions.
  5. Adds three types of lighting: ambient light (AmbientLight), directional light (DirectionalLight), and point light (PointLight) for varied effects.
  6. Applies exponential fog (FogExp2), causing objects to fade into black as they move farther from the camera.

Objects

In Three.js, creating and manipulating 3D objects typically involves creating a geometry, defining a material, combining them into a Mesh, and adding it to the scene.

// Import Three.js library
import * as THREE from 'three';

// Create scene
const scene = new THREE.Scene();

// Create camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5; // Set camera position

// Create renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Create cube geometry
const geometry = new THREE.BoxGeometry(1, 1, 1); // Width, height, depth each 1

// Create red material
const material = new THREE.MeshBasicMaterial({ color: 0xff0000 }); // Red

// Create Mesh (object)
const cube = new THREE.Mesh(geometry, material); // Combine geometry and material into Mesh

// Add Mesh to scene
scene.add(cube);

// Render loop
function animate() {
    requestAnimationFrame(animate);
    renderer.render(scene, camera);
}
animate();

Code Analysis

  1. Import Three.js Library: Uses ES module syntax for importing.
  2. Create Scene: THREE.Scene serves as a container for all 3D objects.
  3. Create Camera: THREE.PerspectiveCamera defines the viewpoint with parameters for field of view, aspect ratio, near, and far clipping planes.
  4. Create Renderer: THREE.WebGLRenderer renders the 3D scene to the screen, with size set and appended to the HTML document.
  5. Create Geometry: THREE.BoxGeometry defines a cube with specified dimensions.
  6. Create Material: THREE.MeshBasicMaterial is the simplest material, set to red (hex value 0xff0000).
  7. Create Mesh: THREE.Mesh forms the 3D object by combining geometry and material.
  8. Add Mesh to Scene: Adds the cube Mesh to the scene.
  9. Render Loop: requestAnimationFrame drives continuous rendering with renderer.render.

Camera

In Three.js, the camera (Camera) is crucial for observing the 3D scene, defining the viewpoint and perspective.

// Create camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

// Set camera position
camera.position.set(0, 3, 5);

Code Analysis

  1. Create Camera: THREE.PerspectiveCamera is the most common camera type, mimicking human vision with perspective. It takes four parameters:
  • fov (Field of View): Angle in degrees. Larger values widen the view but shrink objects; smaller values narrow it.
  • aspect: Aspect ratio, typically window width divided by height.
  • near: Near clipping plane, the closest distance the camera can render.
  • far: Far clipping plane, the farthest distance the camera can render.
  1. Set Camera Position: camera.position.set(x, y, z) positions the camera in 3D space. Here, it’s at (0, 3, 5), meaning 0 units along X, 3 units up Y, and 5 units along Z. Adjust this based on scene needs for optimal viewing.

Other Key Camera Properties and Methods

  • .lookAt(): Orients the camera toward a specific point.
  • .rotateOnAxis(), .rotateX(), .rotateY(), .rotateZ(): Rotate the camera.
  • .zoom: Adjusts focal length, affecting the field of view’s width.

Renderer

In Three.js, the renderer (Renderer) converts the 3D scene into a 2D image displayed on the screen. Below is typical code for creating and configuring a renderer, with key parts explained:

// Create renderer
const renderer = new THREE.WebGLRenderer({
    antialias: true, // Anti-aliasing
});

// Set renderer size
renderer.setSize(window.innerWidth, window.innerHeight);

// Append renderer’s canvas to HTML document
document.body.appendChild(renderer.domElement);

Code Analysis

  • Create Renderer: TRUE.WebGLRenderer is the primary renderer, using the WebGL API to draw 3D graphics. It accepts a configuration object, with antialias: true enabling smoother edges.
  • Set Renderer Size: setSize(width, height) sets the canvas element’s dimensions, typically matching the browser window for full-screen display.
  • Append to Document: renderer.domElement is the canvas element generated by the renderer, appended to the HTML body to display the 3D scene.

Advanced Renderer Settings Include:

  • Clear Color: renderer.clearColor(color, alpha) sets the background color and opacity.
  • Shadow Support: renderer.shadowMap.enabled = true enables shadows.
  • Wireframe Mode: renderer.wireframe = true switches to wireframe mode, showing model outlines.
  • Performance Optimization: renderer.sortObjects = false disables object sorting, potentially improving speed but risking rendering issues.

In the render loop, renderer.render(scene, camera) performs the actual rendering, displaying the scene from the camera’s perspective.

function animate() {
    requestAnimationFrame(animate);
    // Update objects, animations, etc.
    renderer.render(scene, camera);
}
animate();

The animate function runs each frame, ensuring continuous rendering of the 3D scene.

Three.js Geometries and Materials

Basic Geometries

In Three.js, geometries (Geometry) define the foundational shapes of 3D objects, ranging from simple forms like cubes and spheres to complex custom shapes.

Cube (BoxGeometry)

const geometry = new THREE.BoxGeometry(1, 1, 1);

This creates a cube with side lengths of 1. The BoxGeometry parameters represent width, height, and depth.

Sphere (SphereGeometry)

const geometry = new THREE.SphereGeometry(0.5, 32, 32);

This creates a sphere with a radius of 0.5. Parameters are radius, and the number of segments for latitude and longitude.

Cylinder (CylinderGeometry)

const geometry = new THREE.CylinderGeometry(0.5, 1, 1, 32, 1, false);

This creates a cylinder with a bottom radius of 0.5, top radius of 1, and height of 1. Parameters are bottom radius, top radius, height, radial segments, height segments, and whether the ends are open.

Cone (ConeGeometry)

const geometry = new THREE.ConeGeometry(0, 1, 32);

This creates a cone with a bottom radius of 0, top radius of 1, and height of 1. Parameters are bottom radius, top radius, and height segments.

Ring (RingGeometry)

const geometry = new THREE.RingGeometry(0.2, 0.5, 32, 8, Math.PI * 2, 0);

This creates a ring with an inner radius of 0.2 and outer radius of 0.5. Parameters are inner radius, outer radius, theta segments, phi segments, start angle, and arc length.

Plane (PlaneGeometry)

const geometry = new THREE.PlaneGeometry(10, 10, 32, 32);

This creates a plane with a width and height of 10. Parameters are width, height, and segments along the X and Y axes.

Circle (CircleGeometry)

const geometry = new THREE.CircleGeometry(1, 32, 0, Math.PI * 2);

This creates a circular plane with a radius of 1. Parameters are radius, segments, start angle, and arc length.

In practice, geometries are combined with materials to create Mesh objects, which are then added to a scene for rendering. For example:

const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

Custom Geometries

For complex shapes, Three.js allows creating custom geometries by directly manipulating vertices and faces, offering high flexibility despite increased complexity.

const geometry = new THREE.Geometry();

// Add vertices
geometry.vertices.push(new THREE.Vector3(-1, -1, 0));
geometry.vertices.push(new THREE.Vector3(1, -1, 0));
geometry.vertices.push(new THREE.Vector3(0, 1, 0));

// Add face (triangle)
const face = new THREE.Face3(0, 1, 2);
geometry.faces.push(face);

// Compute face normals
geometry.computeFaceNormals();

This code creates a simple triangular geometry, enabling the creation of any shape.

Buffer Geometry (BufferGeometry)

Compared to standard Geometry, BufferGeometry offers significant performance improvements, especially with large vertex counts, by directly manipulating vertex arrays.

const geometry = new THREE.BufferGeometry();

const vertices = new Float32Array([
    -1, -1, 0,
     1, -1, 0,
     0,  1, 0
]);

const vertexBuffer = new THREE.BufferAttribute(vertices, 3);
geometry.setAttribute('position', vertexBuffer);

const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

Geometry Merging and Splitting

Geometry Merging: Combine multiple geometries into one to reduce draw calls and improve performance.

const combinedGeometry = new THREE.Geometry().merge(geometry1);
combinedGeometry.merge(geometry2);

Geometry Splitting: Extracting or splitting parts of a complex geometry often requires complex algorithms. While Three.js doesn’t natively support splitting, libraries like three-bvh can assist.

Dynamic Geometries

For geometries that change at runtime, use BufferGeometry’s .setAttribute() to update vertex data and flag .needsUpdate to refresh.

geometry.attributes.position.array[0] += 0.1; // Modify x-coordinate of first vertex
geometry.attributes.position.needsUpdate = true; // Mark for update

Materials

In Three.js, materials (Material) define the appearance of 3D object surfaces, ranging from simple colors to complex textures and lighting effects.

Basic Material (MeshBasicMaterial)

const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });

This is the simplest material, unaffected by lighting, with a fixed color.

Lambert Material (MeshLambertMaterial)

const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 });

Lambert material accounts for ambient and diffuse lighting but ignores specular reflections.

Phong Material (MeshPhongMaterial)

const material = new THREE.MeshPhongMaterial({
    color: 0x00ff00,
    shininess: 30, // Specular shininess
});

Phong material offers realistic lighting with diffuse, specular, and ambient effects.

Standard Material (MeshStandardMaterial)

const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });

Standard material is a physically based rendering (PBR) model, ideal for PBR scenes, considering metalness, roughness, and more.

Textured Material

const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('path/to/texture.jpg');
const material = new THREE.MeshPhongMaterial({ map: texture });

This uses TextureLoader to load a texture image and applies it to the material’s map property.

Transparent Material (MeshBasicMaterial)

const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.5 });

Set transparent to true and adjust opacity for transparency effects.

Normal Mapped Material

const normalMap = textureLoader.load('path/to/normalMap.jpg');
const material = new THREE.MeshPhongMaterial({
    color: 0x00ff00,
    normalMap: normalMap,
    normalScale: new THREE.Vector2(1, 1), // Controls normal map intensity
});

Normal maps add detail and depth to surfaces.

In practice, materials are combined with geometries to create Mesh objects for rendering. For example:

const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

This creates a cube with the specified material and adds it to the scene.

Beyond these, Three.js offers additional material types and features for diverse rendering needs.

Environment Mapping

const cubeTextureLoader = new THREE.CubeTextureLoader();
const environmentMap = cubeTextureLoader.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);
const material = new THREE.MeshStandardMaterial({ envMap: environmentMap });

Environment mapping simulates surface reflections of the environment using a cube texture loaded with six images.

Emissive Material (MeshEmissiveMaterial)

const material = new THREE.MeshEmissiveMaterial({ emissive: 0xff0000 });

Emissive materials make objects emit light independently of external sources.

Mixed Materials

const materialA = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const materialB = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const mixedMaterial = new THREE.MeshStandardMaterial();
mixedMaterial.blending = THREE.AdditiveBlending;
mixedMaterial.depthWrite = false;
mixedMaterial.opacity = 0.5;
mixedMaterial.transparent = true;
mixedMaterial.color = new THREE.Color().addColors(materialA.color, materialB.color);

Mixing materials creates unique visual effects, here using additive blending for color summation.

Shader Material (ShaderMaterial)

const uniforms = {
    time: { value: 0 },
    resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
};
const vertexShader = /* glsl code */;
const fragmentShader = /* glsl code */;
const material = new THREE.ShaderMaterial({ uniforms, vertexShader, fragmentShader });

Shader materials allow custom vertex and fragment shaders (in GLSL) for highly tailored visuals.

Multi-Materials

const materials = [new THREE.MeshBasicMaterial(), new THREE.MeshBasicMaterial()];
const group = new THREE.Group();
const geometry = new THREE.Geometry();
geometry.merge(new THREE.BoxGeometry(1, 1, 1), new THREE.Matrix4(), 0);
geometry.merge(new THREE.BoxGeometry(1, 1, 1), new THREE.Matrix4().makeTranslation(2, 0, 0), 1);
for (let i = 0; i < geometry.faces.length; i++) {
    geometry.faces[i].materialIndex = i % materials.length;
}
const mesh = new THREE.Mesh(geometry, materials);
group.add(mesh);
scene.add(group);

Multi-materials apply different materials to parts of the same object, here assigning materials to two cubes in a group.

Textures

Textures in Three.js are essential for adding visual detail to 3D object surfaces.

// Load texture
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('path/to/your/image.jpg');

// Create material
const material = new THREE.MeshBasicMaterial({ map: texture });

// Create geometry
const geometry = new THREE.BoxGeometry(1, 1, 1);

// Create Mesh
const mesh = new THREE.Mesh(geometry, material);

// Add to scene
scene.add(mesh);

Code Analysis

  1. Create Texture Loader: THREE.TextureLoader asynchronously loads texture images.
  2. Load Texture: load(url) specifies the texture file URL, returning a Promise that resolves to a THREE.Texture object when loaded.
  3. Create Material: THREE.MeshBasicMaterial accepts a map property set to the texture, applying it to the surface.
  4. Create Geometry: Uses a cube (THREE.BoxGeometry) with side length 1.
  5. Create Mesh: THREE.Mesh combines geometry and material into a 3D entity.
  6. Add to Scene: Adds the Mesh to the THREE.Scene for rendering.

Replace image.jpg with the actual texture path. Textures can be simple images, cube maps (for environment mapping), or others like normal or displacement maps. Texture sizes should match the object’s surface proportions for correct visuals.

Three.js also supports cube textures (CubeTexture), video textures (VideoTexture), and data textures (DataTexture).

Cube Texture (CubeTexture)

const cubeTextureLoader = new THREE.CubeTextureLoader();
cubeTextureLoader.setPath('path/to/cubemap/');
const cubeTexture = cubeTextureLoader.load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);

const material = new THREE.MeshStandardMaterial({ envMap: cubeTexture });

Cube textures, used for environment mapping, consist of six images for positive and negative X, Y, and Z directions.

Video Texture (VideoTexture)

const video = document.createElement('video');
video.src = 'path/to/video.mp4';
video.autoplay = true;
video.loop = true;

const videoTexture = new THREE.VideoTexture(video);
videoTexture.minFilter = THREE.LinearFilter;

const material = new THREE.MeshBasicMaterial({ map: videoTexture });

const geometry = new THREE.PlaneGeometry(10, 10);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

Video textures use a video stream as a surface map, requiring autoplay and looping for continuous playback.

Data Texture (DataTexture)

const data = new Uint8Array(1024 * 1024 * 3); // Assume 1024x1024 RGB data
const dataTexture = new THREE.DataTexture(data, 1024, 1024, THREE.RGBFormat);
dataTexture.needsUpdate = true;

const material = new THREE.MeshBasicMaterial({ map: dataTexture });

Data textures create textures from raw array data, ideal for custom shaders or large datasets.

Three.js Lights and Shadows

Lights

In Three.js, lights (Lights) are essential for simulating illumination, significantly enhancing the realism and depth of a scene.

Ambient Light (AmbientLight)

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); // White ambient light, intensity 0.5
scene.add(ambientLight);

Ambient light uniformly illuminates the entire scene without directionality, ideal for providing baseline lighting.

Point Light (PointLight)

const pointLight = new THREE.PointLight(0xffffff, 1, 100); // White, intensity 1, max distance 100
pointLight.position.set(10, 10, 10); // Set light position
scene.add(pointLight);

Point lights emit light in all directions from a single point, with intensity decreasing over distance.

Spot Light (SpotLight)

const spotLight = new THREE.SpotLight(0xffffff, 1, 100, Math.PI / 4); // White, intensity 1, max distance 100, cone angle 45 degrees
spotLight.position.set(0, 10, 10);
spotLight.target.position.set(0, 0, 0); // Set target position for light direction
scene.add(spotLight);
scene.add(spotLight.target); // Target must also be added to the scene

Spot lights are directional, mimicking flashlights or stage lights, with inner and outer cones for focus and direction control.

Directional Light (DirectionalLight)

const directionalLight = new THREE.DirectionalLight(0xffffff, 1); // White, intensity 1
directionalLight.position.set(0, 1, 1).normalize(); // Set light direction (vector normalized)
scene.add(directionalLight);

Directional lights simulate sunlight, with parallel rays and no attenuation, commonly used for outdoor scenes.

Hemisphere Light (HemisphereLight)

const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x000000, 0.5); // Sky color, ground color, intensity
hemisphereLight.position.set(0, 10, 0); // Typically placed above the scene
scene.add(hemisphereLight);

Hemisphere lights mimic natural lighting from sky and ground, with distinct colors for upper and lower hemispheres.

Key considerations when using lights:

  • Attenuation: Lights like point and spot lights can have decay settings to simulate real-world light falloff.
  • Shadows: Enable with light.castShadow = true and configure shadow properties for added realism.
  • Intensity: Adjust via the constructor’s second parameter or .intensity property.
  • Color: Set using hexadecimal color values in the constructor’s first parameter.

Directional Light Array (DirectionalLightShadow)

const dLight = new THREE.DirectionalLight(0xffffff, 1);
dLight.castShadow = true; // Enable shadows
dLight.shadow.mapSize.width = 1024; // Shadow map size
dLight.shadow.mapSize.height = 1024;
dLight.shadow.camera.top = 10;
dLight.shadow.camera.bottom = -10;
dLight.shadow.camera.left = -10;
dLight.shadow.camera.right = 10;
scene.add(dLight);

This setup creates higher-quality shadows, with shadow camera properties controlling the shadow’s visible area.

Custom Light Source (LightProbe)

const lightProbe = new THREE.LightProbe();
lightProbe.intensity = 1;
lightProbe.environment = cubeEnvMap; // Use cube map as environment light source
scene.add(lightProbe);

Light probes capture environmental lighting for global illumination, suitable for environment mapping and physically based rendering.

Halo Effect (LightHalo)

const halo = new THREE.Halo(light, 100, 100, 100);
halo.color.copy(light.color);
scene.add(halo);

Halo effects simulate glowing rings around light sources, implemented via third-party libraries like three-halo, not built-in.

Light Tracker (TrackballControls)

const controls = new THREE.TrackballControls(camera, renderer.domElement);
controls.update();

While not a light type, TrackballControls enables interactive adjustment of light positions and directions, useful for debugging and previewing lighting.

Light Intensity Gradient (Fog)

scene.fog = new THREE.Fog(0x000000, 1, 100); // Black fog, near plane 1, far plane 100

Fog affects light visibility, creating a gradient effect as objects recede into the distance.

Shadows

In Three.js, shadows involve shadow casting (from light sources) and shadow receiving (by objects). Casting determines if a light produces shadows, while receiving determines if an object displays them.

Enabling Shadow Casting

For shadow-supporting lights (e.g., point, spot, or directional), set castShadow to true.

const light = new THREE.DirectionalLight(0xffffff, 1);
light.castShadow = true;
light.shadow.mapSize.width = 1024; // Shadow map size
light.shadow.mapSize.height = 1024;
// ... Other light settings

Setting Shadow Parameters

Shadow map size impacts quality and performance and can be adjusted as needed.

Enabling Shadow Receiving

To allow objects to display shadows, set their material’s receiveShadow to true.

const material = new THREE.MeshStandardMaterial({ color: 0x000000, receiveShadow: true });
const geometry = new THREE.BoxGeometry(1, 1, 1);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

Shadow Camera and Projection Matrix

Shadow calculations use a separate camera managed by the light. Adjust the shadow.camera properties to fit the scene.

light.shadow.camera.left = -10;
light.shadow.camera.right = 10;
light.shadow.camera.top = 10;
light.shadow.camera.bottom = -10;
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 50;

Shadow Map Updates

Ensure shadow maps are updated each frame.

light.shadow.map.needsUpdate = true;

Renderer Settings

The renderer must enable shadow support.

const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Optional, for smoother shadows

Shadow Map Types

Three.js defaults to PCF (Percentage Closest Filtering) shadow maps. Alternatives include THREE.PCFSoftShadowMap (soft shadows) or THREE.VSMShadowMap (variance shadow maps).

renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Shadow Offset

To fix shadow misalignment, adjust the light’s shadow.bias.

light.shadow.bias = -0.001;

Shadow Blur

Adjust shadow edge softness with renderer.shadowMap.blur.

renderer.shadowMap.blur = true;

Shadow Map Size

Larger shadow maps improve clarity but consume more GPU resources.

light.shadow.mapSize.width = 2048;
light.shadow.mapSize.height = 2048;

Shadow Distance

Limit shadow visibility range with shadow.camera.far.

light.shadow.camera.far = 50;

Shadow Depth Testing

Three.js uses depth maps for shadows by default. For complex geometries, depth textures can improve performance.

light.shadow.map = new THREE.WebGLRenderTarget(1024, 1024, {
    format: THREE.DepthFormat,
    type: THREE.FloatType,
});

Optimizing Shadows

  • Layered Shadows: Group objects and use separate lights and shadow maps per group to reduce computation.
  • Shadow Visibility: Enable shadows only when needed, e.g., when the camera is near an object.

Custom Shadow Shaders

For advanced shadow effects, write custom shaders to process shadow textures.

Three.js Animation and Interaction

Basic Animation

Creating basic animations in Three.js typically involves the following steps.

Create Scene

const scene = new THREE.Scene();

Add Camera

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.z = 5; // Set camera position

Create Renderer

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

Load Model or Geometry

const geometry = new THREE.BoxGeometry(1, 1, 1); // Create a cube

Apply Material

const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); // Green material

Create Mesh

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh); // Add mesh to scene

Animation Section

Create AnimationMixer to manage animations

const mixer = new THREE.AnimationMixer(mesh);

If you have an AnimationClip, add it to the mixer

const clip = ...; // Obtain or load AnimationClip
mixer.clipAction(clip).play(); // Play animation

Update loop to refresh mixer each frame

function animate() {
    requestAnimationFrame(animate);
    mixer.update(delta); // Delta is time difference, typically from THREE.Clock
    renderer.render(scene, camera);
}
animate();

Window Resize Event

To handle window size changes, adjust the renderer’s size:

window.addEventListener('resize', onWindowResize, false);

function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

Create Keyframe Animation

Three.js supports keyframe animations to control object property changes over time. First, create a KeyframeTrack, then build an AnimationClip.

// Define keyframes
const times = [0, 1, 2]; // Keyframe moments on timeline
const values = [0, 1, 0]; // Corresponding keyframe values, e.g., rotation angle
const track = new THREE.NumberKeyframeTrack(
    '.rotation.x', // Path, here it’s the object’s rotation.x property
    times,
    values
);

// Create animation clip
const clip = new THREE.AnimationClip('rotateX', -1, [track]); // -1 for infinite loop

Apply Keyframe Animation

As shown earlier, add the clip to the mixer and play it.

mixer.clipAction(clip).play();

Dynamically Update Animation

To modify animations at runtime, use clipAction methods. For example, to change speed:

mixer.clipAction(clip).setEffectiveTimeScale(2); // Slow down speed

Stop or Reset Animation

To stop or reset animations:

mixer.clipAction(clip).stop(); // Stop animation
mixer.clipAction(clip).reset(); // Reset to initial state

Three.js also allows simple animations using Object3D methods like translation, rotation, or scaling. Here’s an example using translateOnAxis(), rotateOnAxis(), and scale():

function animate(time) {
    requestAnimationFrame(animate);

    // Translation
    mesh.position.x += 0.01;

    // Rotation
    const axis = new THREE.Vector3(0, 1, 0); // Rotate around Y-axis
    const angle = time * 0.01; // Angle proportional to time
    mesh.rotateOnAxis(axis, angle);

    // Scaling
    mesh.scale.set(1 + (time % 2) * 0.1, 1 + (time % 2) * 0.1, 1); // Alternate scaling on X and Y

    renderer.render(scene, camera);
}
animate();

This example updates object properties directly in the animate function without AnimationClip or AnimationMixer. It’s sufficient for simple effects, but for complex interactions or sequenced animations, AnimationMixer offers more control and blending options.

Tween.js Animation

Tween.js is a standalone JavaScript library for smooth transitions, often used with Three.js for object animations.

Tween.js Animation Basics

// Import Tween.js
import { Tween } from 'three/examples/jsm/libs/tween.module.js';

// Create a Three.js object, e.g., a cube
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);

// Tween.js animation
const tween = new TWEEN.Tween(cube.position) // Select property to animate
    .to({ x: 5, y: 5, z: 5 }, 2000) // Target position, 2-second duration
    .easing(TWEEN.Easing.Quadratic.InOut) // Quadratic InOut easing
    .onUpdate(() => { // Update callback, called each frame
        scene.remove(cube);
        scene.add(cube); // Re-add to trigger rendering
    })
    .start(); // Start animation

// Update loop
function animate() {
    requestAnimationFrame(animate);
    TWEEN.update(); // Update Tween.js animations
    renderer.render(scene, camera);
}
animate();

In this example:

  1. A cube is created and added to the scene.
  2. A Tween instance specifies the animated object (cube position) and target position. The to method accepts a target properties object and duration.
  3. Quadratic.InOut easing provides a smooth start and end with faster motion in the middle.
  4. The onUpdate callback runs each frame, removing and re-adding the cube to force Three.js rendering, as position changes alone don’t trigger it.
  5. The animation loop calls TWEEN.update() to refresh all active Tweens.
  6. For modern Three.js, ensure Tween.js compatibility by using tween.module.js instead of older tween.min.js.

Rotation Animation

For rotation animations, use Quaternion to control 3D rotations. Here’s an example:

// Initial rotation
const startRotation = new THREE.Quaternion();

// Target rotation
const endRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI * 2); // Full clockwise rotation

// Tween.js animation
new TWEEN.Tween(startRotation)
    .to(endRotation, 2000) // 2-second duration
    .easing(TWEEN.Easing.Linear.None) // Linear easing
    .onUpdate(() => {
        cube.quaternion.copy(startRotation); // Apply new rotation to cube
        scene.remove(cube);
        scene.add(cube); // Force rendering
    })
    .start();

Color Animation

Animate colors using the Color object. Here’s how to change an object’s color:

// Initial color
const startColor = new THREE.Color(0x00ff00); // Green
cube.material.color.copy(startColor);

// Target color
const endColor = new THREE.Color(0xff0000); // Red

// Tween.js animation
new TWEEN.Tween(startColor)
    .to(endColor, 2000) // 2-second duration
    .easing(TWEEN.Easing.Linear.None) // Linear easing
    .onUpdate(() => {
        cube.material.color.copy(startColor); // Apply new color to material
        scene.remove(cube);
        scene.add(cube); // Force rendering
    })
    .start();

Ensure the material supports color changes (e.g., MeshBasicMaterial or MeshStandardMaterial).

Mouse and Keyboard Controls

In Three.js, mouse and keyboard controls are often implemented using OrbitControls for mouse-driven rotation, panning, and zooming, and custom event listeners for keyboard input.

1. Import and Set Up OrbitControls

Ensure OrbitControls.js is included, typically from examples/jsm/controls/OrbitControls.js.

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

// Create scene, camera, renderer, etc.
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Create OrbitControls instance, linking to camera and renderer’s DOM element
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // Enable damping for smoother rotation
controls.enablePan = true; // Allow panning
controls.enableZoom = true; // Allow zooming

2. Keyboard Controls

Handle keyboard input using window.addEventListener for key press and release events. Here’s an example moving the camera with arrow keys:

let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;

document.addEventListener('keydown', onKeyDown, false);
document.addEventListener('keyup', onKeyUp, false);

function onKeyDown(event) {
    switch (event.keyCode) {
        case 87: // W
            moveForward = true;
            break;
        case 83: // S
            moveBackward = true;
            break;
        case 65: // A
            moveLeft = true;
            break;
        case 68: // D
            moveRight = true;
            break;
    }
}

function onKeyUp(event) {
    switch (event.keyCode) {
        case 87: // W
            moveForward = false;
            break;
        case 83: // S
            moveBackward = false;
            break;
        case 65: // A
            moveLeft = false;
            break;
        case 68: // D
            moveRight = false;
            break;
    }
}

// Apply keyboard control logic in render loop
function animate() {
    requestAnimationFrame(animate);

    if (moveForward) camera.position.z -= 1; // Move forward
    if (moveBackward) camera.position.z += 1; // Move backward
    if (moveLeft) camera.position.x -= 1; // Move left
    if (moveRight) camera.position.x += 1; // Move right

    controls.update(); // Update controls each frame
    renderer.render(scene, camera);
}
animate();

Here, onKeyDown and onKeyUp handle key presses and releases, while animate checks flags to move the camera. Call controls.update() each frame to sync OrbitControls with custom keyboard controls.

3. Smooth Movement

For smoother camera movement, use Tween.js to animate position changes instead of direct updates.

import { Tween, Easing } from 'gsap'; // Or use tween.js; gsap’s TweenMax/TimelineMax is more powerful

function updateCameraPosition(newPosition, duration = 900) {
    const currentPos = { x: camera.position.x, y: camera.position.y, z: camera.position.z };
    const targetPos = { x: newPosition.x, y: newPosition.y, z: newPosition.z };

    new Tween(currentPos)
        .to(targetPos, duration)
        .easing(Easing.Linear.None)
        .onUpdate(() => {
            camera.position.set(currentPos.x, currentPos.y, currentPos.z);
        })
        .start();
}

Use this function in keyboard event handling for smooth transitions:

if (moveForward) updateCameraPosition(new THREE.Vector3(camera.position.x, camera.position.y, camera.position.z - 5));
// Handle other directions similarly

4. Restrict Camera Movement

Prevent the camera from moving beyond a set range by adding bounds checking in updateCameraPosition:

function updateCameraPosition(newPosition, duration = 900) {
    // Add bounds checking
    newPosition.x = Math.max(minX, Math.min(maxX, newPosition.x));
    newPosition.y = Math.max(minY, Math.min(maxY, newPosition.y));
    newPosition.z = Math.max(minZ, Math.min(maxZ, newPosition.z));

    // Rest of the code remains unchanged
}

5. Handle Mouse Wheel Events

Handle mouse wheel zooming via the wheel event:

document.addEventListener('wheel', onWheel, { passive: false });

function onWheel(event) {
    event.preventDefault(); // Prevent default scrolling
    const delta = -event.deltaY * 0.01; // Adjust zoom speed
    camera.position.z += delta; // Adjust camera position based on wheel direction
}

6. Comprehensive Example

Combining these improvements, the final animation loop and event listeners might look like this:

function animate() {
    requestAnimationFrame(animate);

    controls.update(); // Update OrbitControls
    renderer.render(scene, camera);
}

function onWheel(event) {
    event.preventDefault();
    const delta = -event.deltaY * 0.01;
    camera.position.z += delta;
    // May need to update OrbitControls zoom parameters
}

// Initialize event listeners and start animation loop
document.addEventListener('keydown', onKeyDown, false);
document.addEventListener('keyup', onKeyUp, false);
document.addEventListener('wheel', onWheel, { passive: false });
animate();

This example uses gsap’s Tween and Easing, a powerful animation library offering more control than native Tween.js. Choose the animation library based on project needs and preferences.

Share your love