Build a 6-DOF Manipulator in Three.js
A six degree-of-freedom robot arm is the workhorse of industrial
robotics — six revolute joints give it full position and
orientation control over its end-effector. In this tutorial we build
one entirely from Three.js primitives: a nested Group
hierarchy that does forward kinematics for free, live joint sliders,
a gripper, and a small Jacobian-transpose IK solver that lets the arm
chase a draggable target.
1. Scene Setup
Standard Three.js boilerplate: a perspective camera, a renderer, and a grid helper so joint rotations are easy to judge visually. The camera sits at an angle looking down at the arm's base, similar to how you'd frame a real workcell.
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d1117);
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(4, 3, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 1, 0);
scene.add(new THREE.GridHelper(10, 20, 0x334155, 0x1e293b));
scene.add(new THREE.HemisphereLight(0xffffff, 0x223344, 1.2));
const dir = new THREE.DirectionalLight(0xffffff, 1.5);
dir.position.set(3, 5, 2);
scene.add(dir);
2. Defining the Joint Chain
We describe the arm as data first: six joints, each with a link length (distance to the next joint along its local Z after rotation) and a rotation axis, loosely following the same base/ shoulder/elbow/wrist naming used on real industrial arms:
| # | Joint | Axis | Link length |
|---|---|---|---|
| 1 | Base yaw | Y | 0.4 (vertical riser) |
| 2 | Shoulder pitch | X | 1.2 |
| 3 | Elbow pitch | X | 1.0 |
| 4 | Wrist roll | Z | 0.25 |
| 5 | Wrist pitch | X | 0.25 |
| 6 | Wrist yaw (gripper mount) | Y | 0.2 |
const JOINTS = [
{ name: 'base', axis: 'y', length: 0.4 },
{ name: 'shoulder', axis: 'x', length: 1.2 },
{ name: 'elbow', axis: 'x', length: 1.0 },
{ name: 'wristRoll', axis: 'z', length: 0.25 },
{ name: 'wristPitch', axis: 'x', length: 0.25 },
{ name: 'wristYaw', axis: 'y', length: 0.2 },
];
3. Building the Object3D Hierarchy
This is the key trick of doing kinematics in Three.js: instead of
hand-multiplying transform matrices, you build a
nested Group hierarchy where each joint's
Group is a child of the previous link's end. Rotating a
parent's .rotation automatically re-orients everything
nested inside it — Three.js's scene graph is your forward
kinematics chain.
function buildArm(joints) {
const root = new THREE.Group();
const jointGroups = [];
let parent = root;
for (const j of joints) {
const jointGroup = new THREE.Group(); // this Group's rotation IS the joint angle
parent.add(jointGroup);
jointGroups.push(jointGroup);
// visual link: a box or cylinder drawn from this joint to the next
const linkMesh = new THREE.Mesh(
new THREE.CylinderGeometry(0.06, 0.06, j.length, 12),
new THREE.MeshStandardMaterial({ color: 0xfb923c })
);
linkMesh.position.set(0, j.length / 2, 0); // cylinder extends along local +Y
linkMesh.rotation.z = 0;
jointGroup.add(linkMesh);
// the NEXT joint's group is offset by this link's length along local Y
const linkEnd = new THREE.Group();
linkEnd.position.set(0, j.length, 0);
jointGroup.add(linkEnd);
parent = linkEnd; // next joint attaches here
}
return { root, jointGroups, endEffector: parent };
}
const { root, jointGroups, endEffector } = buildArm(JOINTS);
scene.add(root);
Group separate from its visual mesh means you
can rotate jointGroup.rotation[axis] directly without
worrying about the mesh's own offset/pivot — the mesh is just a
child decoration of the joint.
4. Forward Kinematics via World Matrices
Because endEffector is a real node in the scene graph,
Three.js has already computed its forward kinematics for you every
time it updates world matrices. You never need to hand-multiply
the chain of Denavit-Hartenberg matrices — just read them back out:
function getEndEffectorPose() {
endEffector.updateWorldMatrix(true, false); // refresh this node + ancestors
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
endEffector.matrixWorld.decompose(position, quaternion, new THREE.Vector3());
return { position, quaternion };
}
Note the world orientation comes out as a quaternion directly — see our companion article on rotation matrices & quaternions in robotics for why that's exactly the representation you want for smooth end-effector orientation control.
5. Joint Sliders and Live End-Effector Readout
Six HTML range inputs, one per joint, each writing straight into that joint's local rotation on its rotation axis:
JOINTS.forEach((j, i) => {
const slider = document.querySelector(`#joint-${i}`);
slider.addEventListener('input', () => {
const angle = THREE.MathUtils.degToRad(Number(slider.value));
jointGroups[i].rotation[j.axis] = angle;
const { position } = getEndEffectorPose();
document.querySelector('#readout').textContent =
`x=${position.x.toFixed(2)} y=${position.y.toFixed(2)} z=${position.z.toFixed(2)}`;
});
});
6. Adding a Gripper
The gripper is just another child of endEffector — two
finger meshes that open and close by translating along local X,
fully inheriting the wrist's accumulated position and orientation:
function buildGripper() {
const group = new THREE.Group();
const fingerGeo = new THREE.BoxGeometry(0.04, 0.18, 0.04);
const mat = new THREE.MeshStandardMaterial({ color: 0x34d399 });
const left = new THREE.Mesh(fingerGeo, mat);
left.position.set(-0.05, 0.09, 0);
const right = left.clone();
right.position.x = 0.05;
group.add(left, right);
group.userData.fingers = [left, right];
return group;
}
const gripper = buildGripper();
endEffector.add(gripper);
function setGripperOpening(t) { // t: 0 (closed) .. 1 (open)
const [left, right] = gripper.userData.fingers;
left.position.x = -0.02 - t * 0.05;
right.position.x = 0.02 + t * 0.05;
}
7. A Simple Jacobian IK Solver
To let the arm chase a draggable target, we compute a numerical Jacobian by finite differences (perturb each joint angle slightly, see how much the end-effector moves) rather than hand-deriving one analytically — much less code, and it works for any joint chain without modification:
function solveIK(target, iterations = 12, alpha = 0.6) {
const EPS = 1e-4;
for (let iter = 0; iter < iterations; iter++) {
const { position: current } = getEndEffectorPose();
const error = new THREE.Vector3().subVectors(target, current);
if (error.length() < 0.01) break; // converged
for (let i = 0; i < jointGroups.length; i++) {
const axis = JOINTS[i].axis;
const original = jointGroups[i].rotation[axis];
// perturb this joint, measure resulting end-effector displacement
jointGroups[i].rotation[axis] = original + EPS;
const { position: perturbed } = getEndEffectorPose();
jointGroups[i].rotation[axis] = original; // restore
const jCol = new THREE.Vector3().subVectors(perturbed, current).divideScalar(EPS);
const dTheta = jCol.dot(error); // Jacobian-transpose update for this joint
jointGroups[i].rotation[axis] = original + alpha * dTheta;
}
}
}
8. Animation Loop
Finally, tie it together: each frame, if IK mode is active, nudge the joints toward the target, then render.
let ikTarget = new THREE.Vector3(1.2, 1.5, 0.8);
let ikEnabled = true;
function animate() {
requestAnimationFrame(animate);
if (ikEnabled) solveIK(ikTarget);
controls.update();
renderer.render(scene, camera);
}
animate();
From here, natural extensions include clamping each joint to realistic limits (e.g. ±170° for the base, ±120° for the elbow), swapping the finite-difference Jacobian for the analytic version from our Jacobian IK vs FABRIK comparison, or driving the arm along a full trajectory with quaternion SLERP between waypoint orientations.
Frequently Asked Questions
What will I learn in this tutorial?
Build a 6 degree-of-freedom robot arm in Three.js from scratch: a joint hierarchy of Object3D groups, forward kinematics, joint-angle sliders, and a simple Jacobian transpose IK solver.
What topics are covered in this tutorial?
This tutorial covers: Scene Setup, Defining the Joint Chain, Building the Object3D Hierarchy, Forward Kinematics via World Matrices, Joint Sliders and Live Readout, Adding a Gripper, A Simple Jacobian IK Solver, Animation Loop.
How long does this tutorial take?
This tutorial takes approximately 35 minutes to complete.
What prerequisites do I need before starting?
This is a Intermediate – Advanced-level tutorial — no special preparation beyond basic JavaScript is assumed.