
Threejs Core Math
- 19 installs
- 11 repo stars
- Updated July 8, 2026
- openaec-foundation/three.js-claude-skill-package
Helps with ai & agent building tasks.
About
threejs-core-math is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- threejs-core-math
- AI & Agent Building
- AI-coding skill
Threejs Core Math by the numbers
- 19 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #10,587 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/three.js-claude-skill-package --skill threejs-core-mathAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 11 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/three.js-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
threejs-core-math
Quick Reference
Coordinate System
Three.js uses a right-handed coordinate system with Y-up:
| Axis | Direction | Notes |
|---|---|---|
| X | Right | Positive toward screen right |
| Y | Up | Positive toward ceiling |
| Z | Toward viewer | Positive out of the screen |
Import conversion rules:
| Source | Convention | Conversion |
|---|---|---|
| Blender | Z-up | glTF exporter converts automatically |
| FBX | Z-up | FBXLoader converts automatically |
| OBJ | No standard | ALWAYS verify orientation after loading |
| IFC | Z-up | ALWAYS apply -90 degree X rotation or use a converting loader |
NEVER assume imported models match Three.js conventions -- ALWAYS verify orientation after loading.
Core Math Classes
| Class | Purpose | Identity/Default |
|---|---|---|
Vector3 | Position, direction, scale | (0, 0, 0) |
Vector2 | UV coordinates, 2D positions | (0, 0) |
Vector4 | Homogeneous coordinates, shader data | (0, 0, 0, 0) |
Matrix4 | 4x4 transformation matrix | Identity matrix |
Quaternion | Rotation without gimbal lock | (0, 0, 0, 1) |
Euler | Human-readable rotation angles | (0, 0, 0, 'XYZ') |
Color | RGB color (0-1 range) | (1, 1, 1) white |
Box3 | Axis-aligned bounding box | Empty (+Inf min, -Inf max) |
Sphere | Bounding sphere | Center origin, radius -1 |
Plane | Infinite plane | Normal (1,0,0), constant 0 |
Ray | Origin + direction | Origin (0,0,0), direction (0,0,-1) |
Frustum | 6-plane view frustum | -- |
Critical Warnings
NEVER modify a Vector3/Matrix4/Quaternion that is shared between objects -- most methods mutate in-place. ALWAYS use .clone() before modifying shared instances.
// WRONG: mutates the shared vector
const offset = new Vector3(1, 0, 0);
meshA.position.add(offset);
meshB.position.add(offset); // offset is still (1, 0, 0) BUT if you
// had stored meshA.position somewhere,
// it would be mutated
// CORRECT: clone before mutation
const pos = sharedPosition.clone().add(offset);NEVER interpolate Euler angles directly -- it produces incorrect rotation paths and triggers gimbal lock. ALWAYS convert to Quaternion, use slerp(), then convert back if needed.
NEVER manually set Quaternion x, y, z, w values unless you understand quaternion math. ALWAYS use setFromAxisAngle(), setFromEuler(), or slerp().
NEVER use lerp() on Color for hue-shifting animations -- it produces muddy intermediate colors. ALWAYS use lerpHSL() for transitions through different hues.
ALWAYS use MathUtils.degToRad() when Three.js expects radians -- all rotation methods use radians, not degrees.
---
Vector3
The most-used math class. All mutating methods return this for chaining.
Arithmetic: add(v), addScalar(s), sub(v), multiply(v), multiplyScalar(s), divide(v), divideScalar(s), negate()
Geometric: dot(v): number, cross(v), length(): number, lengthSq(): number, normalize(), setLength(l), reflect(normal), project(camera), unproject(camera)
Distance: distanceTo(v): number, distanceToSquared(v): number, manhattanDistanceTo(v): number
Interpolation: lerp(v, alpha), lerpVectors(v1, v2, t), clamp(min, max), clampLength(min, max)
Transform: applyMatrix4(m), applyQuaternion(q), applyAxisAngle(axis, angle), applyEuler(euler)
Conversion: setFromMatrixPosition(m), setFromMatrixScale(m), setFromSphericalCoords(r, phi, theta), toArray(arr?, offset?), fromArray(arr, offset?)
Assignment: set(x, y, z), copy(v), clone(), equals(v): boolean
Vector2 differs: has only x, y. No cross() (returns scalar via cross(v): number). No 3D transforms.
Vector4 differs: has x, y, z, w. Used for homogeneous coordinates and shader uniforms.
---
Matrix4
Column-major storage (WebGL convention). 16 floats in elements array:
elements[0] elements[4] elements[8] elements[12] // Translation = [12,13,14]
elements[1] elements[5] elements[9] elements[13]
elements[2] elements[6] elements[10] elements[14]
elements[3] elements[7] elements[11] elements[15]Composition: compose(position, quaternion, scale) / decompose(position, quaternion, scale) -- the standard TRS (Translate-Rotate-Scale) pattern.
Multiplication order: Right-to-left. M1.multiply(M2) means M2 is applied first, then M1. Use premultiply(m) for left-multiplication (this = m * this).
Factory methods: makeTranslation(x,y,z), makeScale(x,y,z), makeRotationX(theta), makeRotationY(theta), makeRotationZ(theta), makeRotationAxis(axis, angle), lookAt(eye, target, up)
Operations: invert(), transpose(), determinant(): number, identity(), extractBasis(x, y, z), extractRotation(m), setPosition(v)
---
Quaternion
Represents rotation without gimbal lock. ALWAYS prefer Quaternion over Euler for interpolated animations and compound rotations.
import { Quaternion } from 'three';
const q = new Quaternion(); // identity: (0, 0, 0, 1)Setters: setFromAxisAngle(axis, angle), setFromEuler(euler), setFromRotationMatrix(m), setFromUnitVectors(vFrom, vTo)
Operations: multiply(q) (q applied first, then this), premultiply(q), slerp(qb, t), slerpQuaternions(qa, qb, t), rotateTowards(q, step), conjugate(), invert(), normalize()
Comparison: dot(q): number, angleTo(q): number, equals(q): boolean
---
Euler
Human-readable rotation in radians with a rotation order.
import { Euler } from 'three';
const e = new Euler(0, Math.PI / 2, 0, 'XYZ');Rotation orders: 'XYZ' (default), 'YXZ', 'ZXY', 'ZYX', 'YZX', 'XZY'
Gimbal lock: In 'XYZ' order, gimbal lock occurs at Y = +/- 90 degrees. Symptoms: unexpected snapping, loss of one rotational axis.
Methods: setFromRotationMatrix(m), setFromQuaternion(q, order?), reorder(newOrder), equals(euler): boolean
Euler vs Quaternion Decision Tree
| Scenario | Use |
|---|---|
| Setting a fixed rotation | Euler -- human-readable |
| Smooth rotation animation | Quaternion + slerp() |
| Combining multiple rotations | Quaternion + multiply() |
| Avoiding gimbal lock | Quaternion |
| Reading rotation from user input (degrees) | Euler, convert via quaternion.setFromEuler(euler) |
| Storing rotation in scene graph | object.rotation (Euler) auto-syncs with object.quaternion |
---
Color
import { Color } from 'three';
new Color(0xff0000); // hex integer
new Color('red'); // CSS color name
new Color('rgb(255, 0, 0)'); // CSS rgb string
new Color('#ff0000'); // CSS hex string
new Color('hsl(0, 100%, 50%)'); // CSS hsl string
new Color(1.0, 0.0, 0.0); // RGB floats (0-1 range)Properties: r, g, b (number, 0-1 range).
Setters: set(value), setHex(hex), setRGB(r,g,b), setHSL(h,s,l), setStyle(css), setColorName(name)
Getters: getHex(): number, getHexString(): string, getHSL(target): {h,s,l}, getStyle(): string
Interpolation: lerp(color, alpha) (RGB), lerpHSL(color, alpha) (perceptually better for hue shifts), lerpColors(c1, c2, alpha)
Color space: convertSRGBToLinear(), convertLinearToSRGB() -- ALWAYS convert texture colors to linear space for physically correct rendering.
---
MathUtils
Static utility methods -- NEVER instantiate, ALWAYS access via MathUtils.method().
import { MathUtils } from 'three';
MathUtils.degToRad(90); // 1.5707...
MathUtils.clamp(value, 0, 1); // clamp to range
MathUtils.lerp(0, 100, 0.5); // 50
MathUtils.mapLinear(5, 0, 10, 0, 100); // 50
MathUtils.smoothstep(0.5, 0, 1); // Hermite ease
MathUtils.damp(current, target, lambda, dt); // frame-rate-independent damping
MathUtils.generateUUID(); // RFC 4122 v4 UUIDdamp() is particularly useful for smooth camera follow and UI animations -- it produces exponential decay that is frame-rate independent, unlike naive lerp in an animation loop.
---
Bounding Volumes
Box3 (Axis-Aligned Bounding Box)
import { Box3, Vector3 } from 'three';
const box = new Box3();
box.setFromObject(mesh); // compute from mesh
box.containsPoint(new Vector3(1, 2, 3)); // boolean
box.intersectsBox(otherBox); // boolean
box.getCenter(new Vector3()); // center point
box.getSize(new Vector3()); // dimensionsSphere, Plane, Ray, Frustum
- Sphere:
containsPoint(),intersectsBox(),intersectsSphere(),distanceToPoint() - Plane:
distanceToPoint(),projectPoint(),intersectLine() - Ray:
intersectBox(),intersectSphere(),intersectPlane(),distanceToPoint() - Frustum:
setFromProjectionMatrix(m),containsPoint(),intersectsObject(),intersectsBox()
---
Reference Links
- references/methods.md -- Complete method signatures for Vector3, Matrix4, Quaternion, Euler, Color, MathUtils, Box3, Sphere, Plane, Ray
- references/examples.md -- Working code examples for common 3D math operations
- references/anti-patterns.md -- What NOT to do with Three.js math classes
Official Sources
- https://threejs.org/docs/#api/en/math/Vector3
- https://threejs.org/docs/#api/en/math/Matrix4
- https://threejs.org/docs/#api/en/math/Quaternion
- https://threejs.org/docs/#api/en/math/Euler
- https://threejs.org/docs/#api/en/math/Color
- https://threejs.org/docs/#api/en/math/MathUtils
- https://threejs.org/docs/#api/en/math/Box3
Anti-Patterns (Three.js Math)
1. Mutating Shared Vectors
// WRONG: Modifying a vector that is referenced by multiple objects
const origin = new Vector3(0, 0, 0);
meshA.position.copy(origin);
meshB.position.copy(origin);
// Later: accidentally mutating the "constant"
origin.set(10, 0, 0); // origin is now (10, 0, 0)
// meshA and meshB positions are NOT affected (they were copied)
// BUT this pattern is dangerous when passing vectors to methods:
const direction = new Vector3(0, 1, 0);
const reflected = direction.reflect(wallNormal); // MUTATES direction!
// direction is now the reflected vector, not (0, 1, 0) anymore
// CORRECT: ALWAYS clone before mutation when the original must be preserved
const reflected = direction.clone().reflect(wallNormal);WHY: Nearly all Vector3/Matrix4/Quaternion methods mutate this in-place and return this for chaining. Forgetting to clone() before calling a mutating method silently corrupts shared data.
---
2. Lerping Euler Angles Directly
// WRONG: Linear interpolation of Euler angles
function animateRotation(mesh, startEuler, endEuler, t) {
mesh.rotation.x = startEuler.x + (endEuler.x - startEuler.x) * t;
mesh.rotation.y = startEuler.y + (endEuler.y - startEuler.y) * t;
mesh.rotation.z = startEuler.z + (endEuler.z - startEuler.z) * t;
}
// CORRECT: Convert to quaternions, slerp, let auto-sync handle the rest
import { Quaternion, Euler } from 'three';
const qStart = new Quaternion().setFromEuler(startEuler);
const qEnd = new Quaternion().setFromEuler(endEuler);
function animateRotation(mesh, t) {
mesh.quaternion.slerpQuaternions(qStart, qEnd, t);
// mesh.rotation (Euler) auto-syncs from mesh.quaternion
}WHY: Euler angle interpolation does NOT follow the shortest rotational path. It can produce wobbly motion, unexpected detours, and triggers gimbal lock when any axis crosses +/- 90 degrees. Quaternion slerp() ALWAYS takes the shortest arc.
---
3. Using Degrees Instead of Radians
// WRONG: Three.js expects radians, not degrees
mesh.rotation.y = 90; // This rotates ~14.3 full turns, NOT 90 degrees!
// CORRECT: ALWAYS convert degrees to radians
import { MathUtils } from 'three';
mesh.rotation.y = MathUtils.degToRad(90); // 1.5707... radians = 90 degrees
// Also correct: use Math.PI directly
mesh.rotation.y = Math.PI / 2; // 90 degreesWHY: All Three.js rotation methods (Euler, Quaternion.setFromAxisAngle, Matrix4.makeRotationX) use radians. Passing degree values produces wildly incorrect rotations with no error message.
---
4. Wrong Matrix Multiplication Order
// WRONG: Expecting left-to-right application order
const rotate = new Matrix4().makeRotationY(Math.PI / 4);
const translate = new Matrix4().makeTranslation(10, 0, 0);
// This translates FIRST, then rotates (right-to-left!)
const result = rotate.multiply(translate);
// CORRECT: If you want rotate first, then translate:
const result = translate.clone().multiply(rotate);
// Or equivalently:
const result = rotate.clone().premultiply(translate);WHY: Three.js uses the mathematical convention where A.multiply(B) computes A * B, which applies B first, then A. This is the opposite of reading order. Use premultiply() when you need left-multiplication.
---
5. Manually Setting Quaternion Components
// WRONG: Setting quaternion values without understanding quaternion math
mesh.quaternion.set(0, 0.5, 0, 0.5); // What rotation is this? Unclear and likely wrong.
// Also: this is NOT normalized, which produces scaling artifacts
// CORRECT: Use semantic setter methods
mesh.quaternion.setFromAxisAngle(new Vector3(0, 1, 0), Math.PI / 2);
// OR
mesh.quaternion.setFromEuler(new Euler(0, Math.PI / 2, 0));WHY: Quaternion components (x, y, z, w) do NOT correspond to rotation axes or angles in any intuitive way. Setting them manually almost always produces incorrect rotations and unnormalized quaternions (which cause mesh distortion).
---
6. Forgetting to Update World Matrix Before Reading
// WRONG: Reading world position without updating matrices first
mesh.position.set(5, 0, 0);
const worldPos = new Vector3().setFromMatrixPosition(mesh.matrixWorld);
// worldPos may still be (0, 0, 0) if matrices have not been updated!
// CORRECT: ALWAYS call updateMatrixWorld before reading world-space values
mesh.updateMatrixWorld(true); // force update entire hierarchy
const worldPos = new Vector3().setFromMatrixPosition(mesh.matrixWorld);
// Also correct: use getWorldPosition helper
const worldPos = new Vector3();
mesh.getWorldPosition(worldPos);
// getWorldPosition calls updateWorldMatrix internallyWHY: Three.js defers matrix updates to the render loop for performance. If you read matrixWorld outside the render cycle (e.g., after setting position but before render), it contains stale data. Either call updateMatrixWorld(true) or use the getWorld* helper methods.
---
7. Using Color.lerp for Hue Transitions
// WRONG: RGB lerp between colors with different hues
const red = new Color(0xff0000);
const blue = new Color(0x0000ff);
const mid = red.clone().lerp(blue, 0.5);
// Result: (0.5, 0, 0.5) = dark purple/muddy magenta
// CORRECT: Use lerpHSL for perceptually smooth hue transitions
const mid = red.clone().lerpHSL(blue, 0.5);
// Result: transitions through the hue wheel (green/cyan region)WHY: lerp() interpolates R, G, B channels independently, which produces desaturated, muddy intermediate colors when hues differ significantly. lerpHSL() interpolates in HSL space, preserving saturation and producing vibrant transitions.
---
8. Creating Vectors/Matrices Inside Animation Loops
// WRONG: Allocating new objects every frame causes GC pressure
function animate() {
const direction = new Vector3(0, 0, -1); // NEW object every frame
direction.applyQuaternion(camera.quaternion);
player.position.add(direction.multiplyScalar(speed));
requestAnimationFrame(animate);
}
// CORRECT: Reuse pre-allocated objects
const _direction = new Vector3(); // allocate once, outside the loop
function animate() {
_direction.set(0, 0, -1);
_direction.applyQuaternion(camera.quaternion);
player.position.add(_direction.multiplyScalar(speed));
requestAnimationFrame(animate);
}WHY: Creating temporary Vector3/Matrix4/Quaternion objects in a 60fps loop generates thousands of short-lived objects per second. This triggers frequent garbage collection pauses, causing visible frame drops (jank). ALWAYS pre-allocate reusable temporary objects outside the loop.
---
9. Ignoring Gimbal Lock with Euler Rotations
// WRONG: Using Euler angles for a flight simulator camera
// When pitch reaches 90 degrees, yaw and roll merge -- gimbal lock!
camera.rotation.order = 'XYZ';
camera.rotation.x += pitchInput; // pitch
camera.rotation.y += yawInput; // yaw
camera.rotation.z += rollInput; // roll
// At rotation.x = Math.PI/2, changing y and z produce the same rotation
// CORRECT: Use quaternion incremental rotation
const pitchQ = new Quaternion().setFromAxisAngle(
new Vector3(1, 0, 0), pitchInput
);
const yawQ = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0), yawInput
);
const rollQ = new Quaternion().setFromAxisAngle(
new Vector3(0, 0, 1), rollInput
);
camera.quaternion.multiply(yawQ);
camera.quaternion.multiply(pitchQ);
camera.quaternion.multiply(rollQ);
camera.quaternion.normalize(); // ALWAYS normalize after compound multipliesWHY: Euler angles have an inherent singularity (gimbal lock) when the middle axis rotation reaches +/- 90 degrees. For any application requiring free 3D rotation (flight sims, space games, 6DOF controllers), ALWAYS use quaternions with incremental multiplication.
---
10. Not Normalizing Quaternions After Repeated Multiplication
// WRONG: Compound quaternion operations without normalization
function update() {
const delta = new Quaternion().setFromAxisAngle(axis, smallAngle);
mesh.quaternion.multiply(delta);
// After hundreds of frames, floating-point drift makes the quaternion
// non-unit, causing mesh scaling/shearing artifacts
}
// CORRECT: Normalize periodically or after compound operations
function update() {
const delta = new Quaternion().setFromAxisAngle(axis, smallAngle);
mesh.quaternion.multiply(delta);
mesh.quaternion.normalize(); // prevents drift accumulation
}WHY: Quaternions must be unit-length (length = 1) to represent pure rotations. Floating-point arithmetic introduces tiny errors on each multiply. Over hundreds of frames, these accumulate and the quaternion drifts from unit length, causing visible mesh distortion. ALWAYS normalize after repeated multiplications.
Working Code Examples (Three.js Math)
Example 1: Vector3 Operations -- Position, Distance, and Direction
import { Vector3 } from 'three';
// Create positions
const playerPos = new Vector3(10, 0, 5);
const enemyPos = new Vector3(20, 0, 15);
// Calculate distance between two points
const distance = playerPos.distanceTo(enemyPos);
console.log(distance); // ~14.14
// Calculate direction from player to enemy (normalized)
const direction = new Vector3().subVectors(enemyPos, playerPos).normalize();
console.log(direction); // approximately (0.707, 0, 0.707)
// Move player toward enemy by 2 units
const moveSpeed = 2;
const movement = direction.clone().multiplyScalar(moveSpeed);
playerPos.add(movement);
// playerPos is now approximately (11.41, 0, 6.41)
// Project a point onto a vector (closest point on a line)
const lineDir = new Vector3(1, 0, 0); // X axis
const point = new Vector3(3, 4, 0);
const projected = point.clone().projectOnVector(lineDir);
console.log(projected); // (3, 0, 0)
// Reflect a velocity vector off a wall
const velocity = new Vector3(1, 0, -1);
const wallNormal = new Vector3(0, 0, 1);
const reflected = velocity.clone().reflect(wallNormal);
console.log(reflected); // (1, 0, 1)
// Linear interpolation between two positions (50% blend)
const midpoint = new Vector3().lerpVectors(playerPos, enemyPos, 0.5);
// Convert to/from arrays (useful for BufferGeometry)
const arr = playerPos.toArray(); // [x, y, z]
const restored = new Vector3().fromArray(arr);---
Example 2: Matrix4 -- Compose, Decompose, and Custom Transforms
import { Vector3, Quaternion, Matrix4, MathUtils } from 'three';
// Build a transformation matrix from position, rotation, scale
const position = new Vector3(5, 10, 0);
const quaternion = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0), // Y axis
MathUtils.degToRad(45) // 45 degrees
);
const scale = new Vector3(2, 2, 2);
const matrix = new Matrix4();
matrix.compose(position, quaternion, scale);
// Decompose back to components
const outPos = new Vector3();
const outQuat = new Quaternion();
const outScale = new Vector3();
matrix.decompose(outPos, outQuat, outScale);
// outPos = (5, 10, 0), outScale = (2, 2, 2)
// Chain transformations: rotate then translate
// Remember: right-to-left order. multiply(M2) means M2 first, then this.
const rotMatrix = new Matrix4().makeRotationY(MathUtils.degToRad(90));
const transMatrix = new Matrix4().makeTranslation(10, 0, 0);
const combined = transMatrix.clone().multiply(rotMatrix);
// Result: first rotates 90 deg around Y, then translates 10 on X
// Transform a point with a matrix
const point = new Vector3(1, 0, 0);
point.applyMatrix4(combined);
// Point is rotated then translated
// Extract translation from a model's world matrix
const worldPos = new Vector3().setFromMatrixPosition(mesh.matrixWorld);
// Invert a matrix (useful for world-to-local conversion)
const inverseWorld = mesh.matrixWorld.clone().invert();
const localPoint = worldPos.clone().applyMatrix4(inverseWorld);---
Example 3: Quaternion Rotation -- Slerp Animation and Compound Rotations
import { Quaternion, Vector3, MathUtils } from 'three';
// Create rotation from axis + angle
const q1 = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0), // Y axis
MathUtils.degToRad(0) // Starting rotation
);
const q2 = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0), // Y axis
MathUtils.degToRad(180) // Target rotation
);
// Smooth rotation interpolation in animation loop
function animate() {
const t = (Math.sin(Date.now() * 0.001) + 1) / 2; // oscillate 0-1
mesh.quaternion.slerpQuaternions(q1, q2, t);
}
// Combine two rotations (order matters!)
const pitchUp = new Quaternion().setFromAxisAngle(
new Vector3(1, 0, 0), // X axis
MathUtils.degToRad(-30) // pitch up 30 degrees
);
const yawRight = new Quaternion().setFromAxisAngle(
new Vector3(0, 1, 0), // Y axis
MathUtils.degToRad(45) // yaw right 45 degrees
);
// Apply yaw first, then pitch: result = pitch * yaw
const combined = pitchUp.clone().multiply(yawRight);
mesh.quaternion.copy(combined);
// Find rotation that maps one direction to another
const fromDir = new Vector3(0, 0, 1); // forward
const toDir = new Vector3(1, 0, 0); // right
const rotation = new Quaternion().setFromUnitVectors(fromDir, toDir);
// rotation now represents a 90-degree Y rotation
// Gradually rotate toward a target (useful for turrets, cameras)
const maxStepRadians = MathUtils.degToRad(2); // 2 degrees per frame
mesh.quaternion.rotateTowards(targetQuaternion, maxStepRadians);---
Example 4: Color Operations and Color Space Management
import { Color, MeshStandardMaterial } from 'three';
// Various constructor forms
const red = new Color(0xff0000);
const green = new Color('green');
const blue = new Color(0, 0, 1);
const coral = new Color('#ff7f50');
// HSL-based color creation
const hslColor = new Color();
hslColor.setHSL(0.6, 1.0, 0.5); // bright blue via HSL
// Smooth color transition through the hue wheel
const startColor = new Color(0xff0000); // red
const endColor = new Color(0x0000ff); // blue
// WRONG way: lerp produces muddy brown in the middle
// const muddy = startColor.clone().lerp(endColor, 0.5);
// CORRECT way: lerpHSL goes through the hue wheel
const vibrant = startColor.clone().lerpHSL(endColor, 0.5);
// Result: goes through green/cyan on the way from red to blue
// Read HSL values
const hsl = {};
red.getHSL(hsl);
console.log(hsl); // { h: 0, s: 1, l: 0.5 }
// Color space management for physically correct rendering
const textureColor = new Color(0x808080);
textureColor.convertSRGBToLinear(); // ALWAYS do this for manual color input
// when renderer.outputColorSpace = SRGBColorSpace
// Apply to material
const material = new MeshStandardMaterial({
color: new Color(0x44aa88),
});
// Dynamically change color
material.color.setHex(0xff4444);
material.color.multiplyScalar(0.5); // darken by 50%---
Example 5: Bounding Volumes -- Collision Detection and Frustum Culling
import {
Box3, Sphere, Vector3, Frustum, Matrix4,
PerspectiveCamera, Mesh, BoxGeometry, MeshBasicMaterial
} from 'three';
// Compute bounding box from a mesh
const mesh = new Mesh(
new BoxGeometry(2, 3, 4),
new MeshBasicMaterial()
);
mesh.position.set(5, 0, 0);
mesh.updateMatrixWorld(true); // ALWAYS update before computing bounds
const box = new Box3().setFromObject(mesh);
// box.min ~ (4, -1.5, -2), box.max ~ (6, 1.5, 2)
// Get center and size
const center = new Vector3();
const size = new Vector3();
box.getCenter(center); // (5, 0, 0)
box.getSize(size); // (2, 3, 4)
// Point containment test
const testPoint = new Vector3(5, 0, 0);
console.log(box.containsPoint(testPoint)); // true
// Box-box intersection (AABB collision detection)
const otherBox = new Box3(
new Vector3(3, -1, -1),
new Vector3(5, 1, 1)
);
console.log(box.intersectsBox(otherBox)); // true
// Bounding sphere from box
const sphere = new Sphere();
box.getBoundingSphere(sphere);
// Frustum culling: check if object is visible to camera
const camera = new PerspectiveCamera(75, 16/9, 0.1, 1000);
camera.position.set(0, 5, 10);
camera.lookAt(0, 0, 0);
camera.updateMatrixWorld(true);
const frustum = new Frustum();
const projScreenMatrix = new Matrix4();
projScreenMatrix.multiplyMatrices(
camera.projectionMatrix,
camera.matrixWorldInverse
);
frustum.setFromProjectionMatrix(projScreenMatrix);
// Check if a point or box is within the camera view
console.log(frustum.containsPoint(new Vector3(0, 0, 0))); // true if visible
console.log(frustum.intersectsBox(box)); // true if any part is visibleAPI Signatures Reference (Three.js r160+ Math)
Vector3
class Vector3 {
constructor(x?: number, y?: number, z?: number)
// Properties
x: number
y: number
z: number
readonly isVector3: true
// Assignment
set(x: number, y: number, z: number): this
setScalar(scalar: number): this
setX(x: number): this
setY(y: number): this
setZ(z: number): this
setComponent(index: 0 | 1 | 2, value: number): this
getComponent(index: 0 | 1 | 2): number
copy(v: Vector3): this
clone(): Vector3
// Arithmetic
add(v: Vector3): this
addScalar(s: number): this
addVectors(a: Vector3, b: Vector3): this
addScaledVector(v: Vector3, s: number): this
sub(v: Vector3): this
subScalar(s: number): this
subVectors(a: Vector3, b: Vector3): this
multiply(v: Vector3): this
multiplyScalar(s: number): this
multiplyVectors(a: Vector3, b: Vector3): this
divide(v: Vector3): this
divideScalar(s: number): this
negate(): this
// Geometric
dot(v: Vector3): number
cross(v: Vector3): this
crossVectors(a: Vector3, b: Vector3): this
length(): number
lengthSq(): number
manhattanLength(): number
normalize(): this
setLength(length: number): this
reflect(normal: Vector3): this
angleTo(v: Vector3): number
projectOnVector(v: Vector3): this
projectOnPlane(planeNormal: Vector3): this
project(camera: Camera): this
unproject(camera: Camera): this
// Distance
distanceTo(v: Vector3): number
distanceToSquared(v: Vector3): number
manhattanDistanceTo(v: Vector3): number
// Interpolation
lerp(v: Vector3, alpha: number): this
lerpVectors(v1: Vector3, v2: Vector3, alpha: number): this
clamp(min: Vector3, max: Vector3): this
clampLength(min: number, max: number): this
clampScalar(minVal: number, maxVal: number): this
// Transform
applyMatrix3(m: Matrix3): this
applyMatrix4(m: Matrix4): this
applyNormalMatrix(m: Matrix3): this
applyQuaternion(q: Quaternion): this
applyAxisAngle(axis: Vector3, angle: number): this
applyEuler(euler: Euler): this
transformDirection(m: Matrix4): this
// Conversion
setFromMatrixPosition(m: Matrix4): this
setFromMatrixScale(m: Matrix4): this
setFromMatrixColumn(m: Matrix4, index: number): this
setFromMatrix3Column(m: Matrix3, index: number): this
setFromSpherical(s: Spherical): this
setFromSphericalCoords(radius: number, phi: number, theta: number): this
setFromCylindrical(c: Cylindrical): this
setFromCylindricalCoords(radius: number, theta: number, y: number): this
// Utility
equals(v: Vector3): boolean
toArray(array?: number[], offset?: number): number[]
fromArray(array: number[], offset?: number): this
fromBufferAttribute(attribute: BufferAttribute, index: number): this
min(v: Vector3): this
max(v: Vector3): this
floor(): this
ceil(): this
round(): this
roundToZero(): this
random(): this
}Key behavior: All methods that return this mutate the instance in-place. ALWAYS use clone() before modifying a shared vector.
---
Vector2
class Vector2 {
constructor(x?: number, y?: number)
x: number
y: number
// Same arithmetic/utility pattern as Vector3 but 2D only
set(x: number, y: number): this
add(v: Vector2): this
sub(v: Vector2): this
multiply(v: Vector2): this
multiplyScalar(s: number): this
divide(v: Vector2): this
divideScalar(s: number): this
dot(v: Vector2): number
cross(v: Vector2): number // Returns scalar (2D cross product)
length(): number
lengthSq(): number
normalize(): this
lerp(v: Vector2, alpha: number): this
distanceTo(v: Vector2): number
angle(): number // Angle in radians from positive X axis
rotateAround(center: Vector2, angle: number): this
clone(): Vector2
copy(v: Vector2): this
equals(v: Vector2): boolean
toArray(array?: number[], offset?: number): number[]
fromArray(array: number[], offset?: number): this
}---
Vector4
class Vector4 {
constructor(x?: number, y?: number, z?: number, w?: number)
x: number
y: number
z: number
w: number
// Same arithmetic pattern as Vector3 but 4D
set(x: number, y: number, z: number, w: number): this
add(v: Vector4): this
sub(v: Vector4): this
multiplyScalar(s: number): this
divideScalar(s: number): this
dot(v: Vector4): number
length(): number
normalize(): this
lerp(v: Vector4, alpha: number): this
applyMatrix4(m: Matrix4): this // Transforms as homogeneous coordinate
clone(): Vector4
copy(v: Vector4): this
equals(v: Vector4): boolean
}---
Matrix4
class Matrix4 {
constructor()
// Properties
elements: number[] // 16 floats, column-major order
readonly isMatrix4: true
// Assignment
set(
n11: number, n12: number, n13: number, n14: number,
n21: number, n22: number, n23: number, n24: number,
n31: number, n32: number, n33: number, n34: number,
n41: number, n42: number, n43: number, n44: number
): this
identity(): this
copy(m: Matrix4): this
clone(): Matrix4
// Composition
compose(position: Vector3, quaternion: Quaternion, scale: Vector3): this
decompose(position: Vector3, quaternion: Quaternion, scale: Vector3): this
// Multiplication
multiply(m: Matrix4): this // this = this * m (right-multiply)
premultiply(m: Matrix4): this // this = m * this (left-multiply)
multiplyMatrices(a: Matrix4, b: Matrix4): this // this = a * b
multiplyScalar(s: number): this
// Factory methods
makeTranslation(x: number, y: number, z: number): this
makeTranslation(v: Vector3): this
makeScale(x: number, y: number, z: number): this
makeRotationX(theta: number): this
makeRotationY(theta: number): this
makeRotationZ(theta: number): this
makeRotationAxis(axis: Vector3, angle: number): this
makeRotationFromEuler(euler: Euler): this
makeRotationFromQuaternion(q: Quaternion): this
makeBasis(xAxis: Vector3, yAxis: Vector3, zAxis: Vector3): this
lookAt(eye: Vector3, target: Vector3, up: Vector3): this
makePerspective(
left: number, right: number, top: number, bottom: number,
near: number, far: number
): this
makeOrthographic(
left: number, right: number, top: number, bottom: number,
near: number, far: number
): this
// Operations
determinant(): number
invert(): this
transpose(): this
extractBasis(xAxis: Vector3, yAxis: Vector3, zAxis: Vector3): this
extractRotation(m: Matrix4): this
setPosition(x: number, y: number, z: number): this
setPosition(v: Vector3): this
getMaxScaleOnAxis(): number
// Comparison / Conversion
equals(m: Matrix4): boolean
toArray(array?: number[], offset?: number): number[]
fromArray(array: number[], offset?: number): this
}Column-major layout:
elements[0] elements[4] elements[8] elements[12] // m11 m12 m13 tx
elements[1] elements[5] elements[9] elements[13] // m21 m22 m23 ty
elements[2] elements[6] elements[10] elements[14] // m31 m32 m33 tz
elements[3] elements[7] elements[11] elements[15] // 0 0 0 1Translation is stored in elements[12], elements[13], elements[14].
---
Quaternion
class Quaternion {
constructor(x?: number, y?: number, z?: number, w?: number)
// Default: (0, 0, 0, 1) = identity rotation
// Properties
x: number
y: number
z: number
w: number
readonly isQuaternion: true
// Assignment
set(x: number, y: number, z: number, w: number): this
identity(): this
copy(q: Quaternion): this
clone(): Quaternion
// Setters (from other representations)
setFromAxisAngle(axis: Vector3, angle: number): this
setFromEuler(euler: Euler): this
setFromRotationMatrix(m: Matrix4): this
setFromUnitVectors(vFrom: Vector3, vTo: Vector3): this
// Operations
multiply(q: Quaternion): this // this = this * q (q applied first)
premultiply(q: Quaternion): this // this = q * this
slerp(qb: Quaternion, t: number): this // spherical linear interpolation
slerpQuaternions(qa: Quaternion, qb: Quaternion, t: number): this
rotateTowards(q: Quaternion, step: number): this
conjugate(): this // negate x, y, z
invert(): this // conjugate / lengthSq
normalize(): this
angleTo(q: Quaternion): number
// Comparison
dot(q: Quaternion): number
length(): number
lengthSq(): number
equals(q: Quaternion): boolean
// Event
_onChange(callback: () => void): this // internal change callback
}---
Euler
class Euler {
constructor(x?: number, y?: number, z?: number, order?: string)
// Default: (0, 0, 0, 'XYZ')
// Properties
x: number // rotation around X axis in radians
y: number // rotation around Y axis in radians
z: number // rotation around Z axis in radians
order: string // 'XYZ' | 'YXZ' | 'ZXY' | 'ZYX' | 'YZX' | 'XZY'
readonly isEuler: true
static DEFAULT_ORDER: 'XYZ'
// Assignment
set(x: number, y: number, z: number, order?: string): this
copy(euler: Euler): this
clone(): Euler
// Setters
setFromRotationMatrix(m: Matrix4, order?: string): this
setFromQuaternion(q: Quaternion, order?: string): this
setFromVector3(v: Vector3, order?: string): this
reorder(newOrder: string): this // changes order, preserves rotation
// Comparison / Conversion
equals(euler: Euler): boolean
toArray(array?: any[], offset?: number): any[] // [x, y, z, order]
fromArray(array: any[]): this
_onChange(callback: () => void): this
}Gimbal lock reference:
| Order | Lock axis | Lock angle |
|---|---|---|
'XYZ' | Y | +/- 90 degrees (pi/2) |
'YXZ' | X | +/- 90 degrees (pi/2) |
'ZXY' | X | +/- 90 degrees (pi/2) |
'ZYX' | Y | +/- 90 degrees (pi/2) |
'YZX' | Z | +/- 90 degrees (pi/2) |
'XZY' | Z | +/- 90 degrees (pi/2) |
---
Color
class Color {
constructor()
constructor(color: Color | string | number)
constructor(r: number, g: number, b: number)
// Properties
r: number // 0-1
g: number // 0-1
b: number // 0-1
readonly isColor: true
// Setters
set(value: Color | string | number): this
setScalar(scalar: number): this
setHex(hex: number, colorSpace?: string): this
setRGB(r: number, g: number, b: number, colorSpace?: string): this
setHSL(h: number, s: number, l: number, colorSpace?: string): this
setStyle(style: string, colorSpace?: string): this
setColorName(name: string): this
copy(color: Color): this
clone(): Color
// Getters
getHex(colorSpace?: string): number
getHexString(colorSpace?: string): string
getHSL(target: { h: number, s: number, l: number }, colorSpace?: string): { h: number, s: number, l: number }
getRGB(target: { r: number, g: number, b: number }, colorSpace?: string): { r: number, g: number, b: number }
getStyle(colorSpace?: string): string
// Color space conversion
convertSRGBToLinear(): this
convertLinearToSRGB(): this
// Arithmetic
add(color: Color): this
addColors(color1: Color, color2: Color): this
addScalar(s: number): this
sub(color: Color): this
multiply(color: Color): this
multiplyScalar(s: number): this
// Interpolation
lerp(color: Color, alpha: number): this
lerpColors(color1: Color, color2: Color, alpha: number): this
lerpHSL(color: Color, alpha: number): this
// Comparison / Conversion
equals(c: Color): boolean
toArray(array?: number[], offset?: number): number[]
fromArray(array: number[], offset?: number): this
toJSON(): number // returns hex
}---
MathUtils
namespace MathUtils {
// Conversion
function degToRad(degrees: number): number
function radToDeg(radians: number): number
// Clamping
function clamp(value: number, min: number, max: number): number
// Interpolation
function lerp(x: number, y: number, t: number): number
function inverseLerp(x: number, y: number, value: number): number
function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number
function smoothstep(x: number, min: number, max: number): number
function smootherstep(x: number, min: number, max: number): number
function damp(x: number, y: number, lambda: number, dt: number): number
function pingpong(x: number, length?: number): number
// Random
function randFloat(low: number, high: number): number
function randFloatSpread(range: number): number
function randInt(low: number, high: number): number
function seededRandom(seed?: number): number
// Power of two
function isPowerOfTwo(value: number): boolean
function ceilPowerOfTwo(value: number): number
function floorPowerOfTwo(value: number): number
// Misc
function generateUUID(): string
function euclideanModulo(n: number, m: number): number
const DEG2RAD: number // Math.PI / 180
const RAD2DEG: number // 180 / Math.PI
}---
Box3
class Box3 {
constructor(min?: Vector3, max?: Vector3)
// Default: min = (+Infinity), max = (-Infinity) = empty box
min: Vector3
max: Vector3
set(min: Vector3, max: Vector3): this
setFromArray(array: number[]): this
setFromBufferAttribute(attribute: BufferAttribute): this
setFromPoints(points: Vector3[]): this
setFromCenterAndSize(center: Vector3, size: Vector3): this
setFromObject(object: Object3D, precise?: boolean): this
clone(): Box3
copy(box: Box3): this
makeEmpty(): this
isEmpty(): boolean
getCenter(target: Vector3): Vector3
getSize(target: Vector3): Vector3
getBoundingSphere(target: Sphere): Sphere
expandByPoint(point: Vector3): this
expandByVector(vector: Vector3): this
expandByScalar(scalar: number): this
expandByObject(object: Object3D, precise?: boolean): this
containsPoint(point: Vector3): boolean
containsBox(box: Box3): boolean
intersectsBox(box: Box3): boolean
intersectsSphere(sphere: Sphere): boolean
intersectsPlane(plane: Plane): boolean
intersectsTriangle(triangle: Triangle): boolean
clampPoint(point: Vector3, target: Vector3): Vector3
distanceToPoint(point: Vector3): number
union(box: Box3): this
intersect(box: Box3): this
equals(box: Box3): boolean
applyMatrix4(matrix: Matrix4): this
translate(offset: Vector3): this
}---
Sphere
class Sphere {
constructor(center?: Vector3, radius?: number)
center: Vector3
radius: number
set(center: Vector3, radius: number): this
setFromPoints(points: Vector3[], optionalCenter?: Vector3): this
clone(): Sphere
copy(sphere: Sphere): this
isEmpty(): boolean
makeEmpty(): this
containsPoint(point: Vector3): boolean
distanceToPoint(point: Vector3): number
intersectsSphere(sphere: Sphere): boolean
intersectsBox(box: Box3): boolean
intersectsPlane(plane: Plane): boolean
clampPoint(point: Vector3, target: Vector3): Vector3
getBoundingBox(target: Box3): Box3
applyMatrix4(matrix: Matrix4): this
translate(offset: Vector3): this
expandByPoint(point: Vector3): this
union(sphere: Sphere): this
equals(sphere: Sphere): boolean
}---
Plane
class Plane {
constructor(normal?: Vector3, constant?: number)
normal: Vector3
constant: number
set(normal: Vector3, constant: number): this
setFromNormalAndCoplanarPoint(normal: Vector3, point: Vector3): this
setFromCoplanarPoints(a: Vector3, b: Vector3, c: Vector3): this
setComponents(x: number, y: number, z: number, w: number): this
normalize(): this
negate(): this
clone(): Plane
copy(plane: Plane): this
distanceToPoint(point: Vector3): number
distanceToSphere(sphere: Sphere): number
projectPoint(point: Vector3, target: Vector3): Vector3
intersectLine(line: Line3, target: Vector3): Vector3 | null
intersectsLine(line: Line3): boolean
intersectsBox(box: Box3): boolean
intersectsSphere(sphere: Sphere): boolean
coplanarPoint(target: Vector3): Vector3
applyMatrix4(matrix: Matrix4, optionalNormalMatrix?: Matrix3): this
translate(offset: Vector3): this
equals(plane: Plane): boolean
}---
Ray
class Ray {
constructor(origin?: Vector3, direction?: Vector3)
origin: Vector3
direction: Vector3
set(origin: Vector3, direction: Vector3): this
clone(): Ray
copy(ray: Ray): this
at(t: number, target: Vector3): Vector3
lookAt(v: Vector3): this
recast(t: number): this
closestPointToPoint(point: Vector3, target: Vector3): Vector3
distanceToPoint(point: Vector3): number
distanceSqToPoint(point: Vector3): number
distanceSqToSegment(
v0: Vector3, v1: Vector3,
optionalPointOnRay?: Vector3, optionalPointOnSegment?: Vector3
): number
intersectBox(box: Box3, target: Vector3): Vector3 | null
intersectsBox(box: Box3): boolean
intersectSphere(sphere: Sphere, target: Vector3): Vector3 | null
intersectsSphere(sphere: Sphere): boolean
intersectPlane(plane: Plane, target: Vector3): Vector3 | null
intersectsPlane(plane: Plane): boolean
intersectTriangle(
a: Vector3, b: Vector3, c: Vector3, backfaceCulling: boolean, target: Vector3
): Vector3 | null
applyMatrix4(matrix4: Matrix4): this
equals(ray: Ray): boolean
}---
Frustum
class Frustum {
constructor(
p0?: Plane, p1?: Plane, p2?: Plane,
p3?: Plane, p4?: Plane, p5?: Plane
)
planes: Plane[] // 6 planes
set(p0: Plane, p1: Plane, p2: Plane, p3: Plane, p4: Plane, p5: Plane): this
setFromProjectionMatrix(m: Matrix4): this
clone(): Frustum
copy(frustum: Frustum): this
containsPoint(point: Vector3): boolean
intersectsObject(object: Object3D): boolean
intersectsSprite(sprite: Sprite): boolean
intersectsSphere(sphere: Sphere): boolean
intersectsBox(box: Box3): boolean
}