Building an IK Robotic Arm in Matter.js

This article provides a complete guide to constructing a 2D physics-driven robotic arm using Matter.js, directed by an Inverse Kinematics (IK) solver. You will learn how to model the physical arm segments using rigid bodies and constraints, calculate target joint angles using analytical IK, and apply motor-like forces to smoothly drive physical joints toward target coordinates while preserving physical collisions and interactions.

1. Setting Up the Physical Arm Hierarchy

A robotic arm in Matter.js consists of multiple rigid bodies connected sequentially by pin constraints. Rather than directly moving positions, the arm must rely on rigid bodies (Matter.Bodies.rectangle) and pivot points (Matter.Constraint.create) to ensure realistic mass distribution and collision response.

const { Engine, Render, Runner, Bodies, Composite, Constraint } = Matter;

const engine = Engine.create();
const world = engine.world;

// Base anchor fixed in world space
const base = Bodies.circle(400, 500, 10, { isStatic: true });

// Arm segments (upper arm and forearm)
const segmentLength = 150;
const segmentWidth = 20;

const upperArm = Bodies.rectangle(400, 425, segmentWidth, segmentLength, {
    collisionFilter: { group: -1 } // Prevent self-collision between links
});

const forearm = Bodies.rectangle(400, 275, segmentWidth, segmentLength, {
    collisionFilter: { group: -1 }
});

// Shoulder joint (Base to Upper Arm)
const shoulderJoint = Constraint.create({
    bodyA: base,
    bodyB: upperArm,
    pointA: { x: 0, y: 0 },
    pointB: { x: 0, y: segmentLength / 2 },
    stiffness: 1,
    length: 0
});

// Elbow joint (Upper Arm to Forearm)
const elbowJoint = Constraint.create({
    bodyA: upperArm,
    bodyB: forearm,
    pointA: { x: 0, y: -segmentLength / 2 },
    pointB: { x: 0, y: segmentLength / 2 },
    stiffness: 1,
    length: 0
});

Composite.add(world, [base, upperArm, forearm, shoulderJoint, elbowJoint]);

2. Implementing the Inverse Kinematics Solver

For a two-link planar arm, analytical trigonometry provides exact target joint angles without the overhead of iterative solvers like FABRIK or CCD. Given a target position \((tx, ty)\) relative to the base \((bx, by)\), calculate the required shoulder angle (\(\theta_1\)) and elbow angle (\(\theta_2\)) using the Law of Cosines.

function solveTwoBoneIK(baseX, baseY, targetX, targetY, l1, l2) {
    const dx = targetX - baseX;
    const dy = targetY - baseY;
    let dist = Math.hypot(dx, dy);

    // Clamp target within reach
    const maxReach = (l1 + l2) * 0.999;
    const minReach = Math.abs(l1 - l2) * 1.001;
    dist = Math.max(minReach, Math.min(dist, maxReach));

    // Law of Cosines
    const cosElbow = (dist * dist - l1 * l1 - l2 * l2) / (2 * l1 * l2);
    const elbowAngle = Math.acos(cosElbow); // Relative angle between segments

    const alpha = Math.atan2(dy, dx);
    const cosShoulder = (dist * dist + l1 * l1 - l2 * l2) / (2 * dist * l1);
    const shoulderAngle = alpha - Math.acos(cosShoulder);

    return {
        shoulderTarget: shoulderAngle + Math.PI / 2,
        elbowTarget: elbowAngle
    };
}

3. Coupling Kinematics to Matter.js Physics

Directly overriding a body's angle via Matter.Body.setAngle() breaks the velocity and impulse resolution of the physics engine, leading to tunneling and jitter. Instead, use a Proportional-Derivative (PD) controller to apply torques or angular velocities toward the target angles.

function applyJointTorque(body, targetAngle, kp = 0.05, kd = 0.01) {
    // Normalize target difference to [-PI, PI]
    let angleDiff = targetAngle - body.angle;
    angleDiff = Math.atan2(Math.sin(angleDiff), Math.cos(angleDiff));

    // PD Controller calculation
    const torque = (angleDiff * kp) - (body.angularVelocity * kd);
    
    // Apply torque directly to the body
    Matter.Body.setAngularVelocity(body, body.angularVelocity + torque);
}

4. Running the Unified Update Loop

Attach the IK calculation and torque application to Matter.js's before-update event hook. This ensures target angles are calculated, physics forces are injected, and collision solvers update sequentially.

const target = { x: 450, y: 350 };

Matter.Events.on(engine, 'beforeUpdate', () => {
    // 1. Solve IK target angles
    const angles = solveTwoBoneIK(
        base.position.x,
        base.position.y,
        target.x,
        target.y,
        segmentLength,
        segmentLength
    );

    // 2. Compute absolute targets
    const targetShoulder = angles.shoulderTarget;
    const targetElbow = upperArm.angle + angles.elbowTarget;

    // 3. Drive bodies using physics-friendly angular velocity adjustments
    applyJointTorque(upperArm, targetShoulder, 0.08, 0.02);
    applyJointTorque(forearm, targetElbow, 0.08, 0.02);
});

By relying on analytical IK to set reference orientations and driving the rigid bodies using angular torque, the arm dynamically follows target positions while naturally interacting with obstacles, pushing objects, and responding to external forces.