Introduction

A robot arm sweeping around a factory floor is a complicated object: every link and joint traces its own arc, and figuring out whether it will crash into a wall means tracking the shape of every part at every moment. That is exhausting to reason about directly.

Configuration space (C-space), introduced by Lozano-Pérez in 1983, cuts through the mess with a single idea: instead of tracking the whole body of the robot, represent its entire pose as a single point in an abstract space. Each axis of that space is one degree of freedom — an angle, a translation, a joint value. The robot's shape disappears; what remains is a point moving through a map.

The price of that simplicity is paid by the obstacles. A wall that the robot must not touch corresponds to a forbidden zone in configuration space — a region that expands to absorb all poses where any part of the robot would collide. Navigate the point around those forbidden zones and you have planned a collision-free path for the full robot body.

This transformation is the foundation of nearly every motion planner ever built, from factory arms to self-driving cars to surgical robots.

Try It

The demo below shows a 2-link planar arm pinned at a fixed base. The arm has two joints, each rotating freely, so its configuration space is a 2-D square — one axis per joint angle (θ1\theta_1 and θ2\theta_2, both in [0°,360°)[0°, 360°)).

<!-- 2-link planar arm: workspace (left) and configuration space (right) -->
<div class="panels">
  <div class="panel">
    <div class="panel-title">Workspace</div>
    <canvas id="ws" width="220" height="220"></canvas>
  </div>
  <div class="panel">
    <div class="panel-title">Configuration Space</div>
    <canvas id="cs" width="220" height="220"></canvas>
  </div>
</div>
<div class="sliders">
  <label>&#952;&#8321; <input id="t1" type="range" min="0" max="359" value="45"> <span id="t1val">45°</span></label>
  <label>&#952;&#8322; <input id="t2" type="range" min="0" max="359" value="90"> <span id="t2val">90°</span></label>
</div>
<div class="hint-box">Drag the sliders to rotate each joint. The arm turns red when it collides with the obstacle. The green dot in the right panel marks the current (θ₁, θ₂) pair — red zone = forbidden configurations.</div>
/* Layout for the two-panel demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; padding: 8px; }
.panels { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
.panel { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.panel-title { font-size: .78rem; font-weight: 600; color: #555; letter-spacing: .02em; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; background: #f4f7fa; display: block; }
.sliders { display: flex; flex-direction: column; gap: 6px; margin: 10px auto; max-width: 460px; }
label { display: flex; align-items: center; gap: 8px; font-size: .88rem; font-weight: 500; }
input[type=range] { flex: 1; accent-color: #1d3557; }
span { min-width: 3em; text-align: right; font-variant-numeric: tabular-nums; }
.hint-box { font-size: .8rem; color: #555; line-height: 1.45; margin-top: 6px; max-width: 460px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));

/* Arm geometry: L1 = upper link length, L2 = forearm length */
const L1 = 70, L2 = 55;
const BASE = { x: 110, y: 110 };

/* Rectangular obstacle in workspace (x, y, width, height) */
const OBS = { x: 130, y: 60, w: 50, h: 40 };

const wsCanvas = document.getElementById('ws');
const csCanvas = document.getElementById('cs');
const wsCtx = wsCanvas.getContext('2d');
const csCtx = csCanvas.getContext('2d');

/* Configuration space: 220×220 pixels, axes = theta1 (x) and theta2 (y) */
const CSPACE_SIZE = 220;
let forbiddenMap = null;

function toRad(deg) { return deg * Math.PI / 180; }

/* Forward kinematics: compute elbow and tip positions from joint angles */
function forwardKinematics(t1Deg, t2Deg) {
  const a1 = toRad(t1Deg);
  const a2 = toRad(t1Deg + t2Deg);
  const elbow = {
    x: BASE.x + L1 * Math.cos(a1),
    y: BASE.y - L1 * Math.sin(a1)
  };
  const tip = {
    x: elbow.x + L2 * Math.cos(a2),
    y: elbow.y - L2 * Math.sin(a2)
  };
  return { elbow, tip };
}

/* Liang-Barsky segment-rectangle intersection test */
function segmentIntersectsRect(ax, ay, bx, by, rx, ry, rw, rh) {
  /* Liang-Barsky parametric clipping loop (p/q per edge) */
  const dx = bx - ax, dy = by - ay;
  let t0 = 0, t1 = 1;
  const ps = [-dx, dx, -dy, dy];
  const qs = [ax - rx, (rx + rw) - ax, ay - ry, (ry + rh) - ay];
  for (let i = 0; i < 4; i++) {
    const p = ps[i], q = qs[i];
    if (Math.abs(p) < 1e-9) { if (q < 0) return false; continue; }
    const r = q / p;
    if (p < 0) { if (r > t1) return false; if (r > t0) t0 = r; }
    else { if (r < t0) return false; if (r < t1) t1 = r; }
  }
  return t0 <= t1;
}

/* Point-in-rectangle helper */
function pointInRect(px, py, rx, ry, rw, rh) {
  return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}

/* Returns true if any part of the arm intersects the obstacle */
function collides(t1Deg, t2Deg) {
  const { elbow, tip } = forwardKinematics(t1Deg, t2Deg);
  /* Check upper link */
  if (segmentIntersectsRect(BASE.x, BASE.y, elbow.x, elbow.y, OBS.x, OBS.y, OBS.w, OBS.h)) return true;
  /* Check forearm */
  if (segmentIntersectsRect(elbow.x, elbow.y, tip.x, tip.y, OBS.x, OBS.y, OBS.w, OBS.h)) return true;
  /* Check joint and tip points */
  if (pointInRect(elbow.x, elbow.y, OBS.x, OBS.y, OBS.w, OBS.h)) return true;
  if (pointInRect(tip.x, tip.y, OBS.x, OBS.y, OBS.w, OBS.h)) return true;
  return false;
}

/* Build (and redraw) the forbidden-zone image for the configuration-space canvas */
function buildForbiddenMap() {
  const imgData = csCtx.createImageData(CSPACE_SIZE, CSPACE_SIZE);
  const map = new Uint8Array(CSPACE_SIZE * CSPACE_SIZE);
  for (let row = 0; row < CSPACE_SIZE; row++) {
    for (let col = 0; col < CSPACE_SIZE; col++) {
      const t1 = (col / CSPACE_SIZE) * 360;
      const t2 = ((CSPACE_SIZE - 1 - row) / CSPACE_SIZE) * 360;
      const hit = collides(t1, t2) ? 1 : 0;
      map[row * CSPACE_SIZE + col] = hit;
      const idx = (row * CSPACE_SIZE + col) * 4;
      if (hit) {
        imgData.data[idx] = 220; imgData.data[idx + 1] = 50; imgData.data[idx + 2] = 60; imgData.data[idx + 3] = 200;
      } else {
        imgData.data[idx] = 240; imgData.data[idx + 1] = 245; imgData.data[idx + 2] = 250; imgData.data[idx + 3] = 255;
      }
    }
  }
  csCtx.putImageData(imgData, 0, 0);
  forbiddenMap = map;

  /* Draw light grid lines every 90 degrees */
  csCtx.save();
  csCtx.strokeStyle = '#aaa';
  csCtx.lineWidth = 0.5;
  for (let i = 1; i < 4; i++) {
    const p = (i / 4) * CSPACE_SIZE;
    csCtx.beginPath(); csCtx.moveTo(p, 0); csCtx.lineTo(p, CSPACE_SIZE); csCtx.stroke();
    csCtx.beginPath(); csCtx.moveTo(0, p); csCtx.lineTo(CSPACE_SIZE, p); csCtx.stroke();
  }
  csCtx.restore();
}

function drawWorkspace(t1Deg, t2Deg) {
  wsCtx.clearRect(0, 0, wsCanvas.width, wsCanvas.height);

  /* Draw the obstacle */
  wsCtx.fillStyle = '#888';
  wsCtx.fillRect(OBS.x, OBS.y, OBS.w, OBS.h);
  wsCtx.strokeStyle = '#555';
  wsCtx.lineWidth = 1;
  wsCtx.strokeRect(OBS.x, OBS.y, OBS.w, OBS.h);

  const { elbow, tip } = forwardKinematics(t1Deg, t2Deg);
  const hit = collides(t1Deg, t2Deg);

  /* Draw arm links (red if colliding) */
  wsCtx.lineCap = 'round';
  wsCtx.lineWidth = 6;
  wsCtx.strokeStyle = hit ? '#e63946' : '#1d3557';
  wsCtx.beginPath();
  wsCtx.moveTo(BASE.x, BASE.y);
  wsCtx.lineTo(elbow.x, elbow.y);
  wsCtx.stroke();

  wsCtx.strokeStyle = hit ? '#e63946' : '#457b9d';
  wsCtx.lineWidth = 5;
  wsCtx.beginPath();
  wsCtx.moveTo(elbow.x, elbow.y);
  wsCtx.lineTo(tip.x, tip.y);
  wsCtx.stroke();

  /* Draw joint circles */
  wsCtx.fillStyle = '#f1faee';
  wsCtx.strokeStyle = '#222';
  wsCtx.lineWidth = 1.5;
  [BASE, elbow].forEach(pt => {
    wsCtx.beginPath();
    wsCtx.arc(pt.x, pt.y, 5, 0, 2 * Math.PI);
    wsCtx.fill(); wsCtx.stroke();
  });
  wsCtx.fillStyle = '#a8dadc';
  wsCtx.beginPath();
  wsCtx.arc(tip.x, tip.y, 4, 0, 2 * Math.PI);
  wsCtx.fill(); wsCtx.stroke();
}

function drawCspacePoint(t1Deg, t2Deg) {
  /* Redraw forbidden zones first */
  buildForbiddenMap();

  const cx = (t1Deg / 360) * CSPACE_SIZE;
  const cy = (1 - t2Deg / 360) * CSPACE_SIZE;
  const hit = collides(t1Deg, t2Deg);

  csCtx.beginPath();
  csCtx.arc(cx, cy, 5, 0, 2 * Math.PI);
  csCtx.fillStyle = hit ? '#e63946' : '#2a9d8f';
  csCtx.fill();
  csCtx.strokeStyle = '#fff';
  csCtx.lineWidth = 1.5;
  csCtx.stroke();

  /* Axis corner labels (theta1 along x, theta2 along y) */
  csCtx.save();
  csCtx.font = '9px system-ui, sans-serif';
  csCtx.fillStyle = '#444';
  csCtx.fillText('0°', 2, CSPACE_SIZE - 2);
  csCtx.fillText('360°', CSPACE_SIZE - 26, CSPACE_SIZE - 2);
  csCtx.fillText('360°', 2, 10);
  csCtx.restore();
}

function update() {
  const t1 = parseInt(document.getElementById('t1').value);
  const t2 = parseInt(document.getElementById('t2').value);
  document.getElementById('t1val').textContent = t1 + '°';
  document.getElementById('t2val').textContent = t2 + '°';
  drawWorkspace(t1, t2);
  drawCspacePoint(t1, t2);
}

document.getElementById('t1').addEventListener('input', update);
document.getElementById('t2').addEventListener('input', update);

/* Initial render */
update();
Result
Console

The left panel shows the arm in physical space with one rectangular obstacle (the gray box). The right panel shows configuration space: the white region is reachable, and the red forbidden zone marks every (θ1,θ2)(\theta_1, \theta_2) pair where the arm would collide with the obstacle. Drag either joint slider and watch both panels update simultaneously — the arm moves on the left, and the point moves on the right.

Notice that a straight path between two points in configuration space may cut through the red zone even when both endpoints look fine. That is why path planning cannot simply interpolate joint angles: it must find a route that stays entirely in the white region.

The Real Complexity

C-space simplifies the representation of a robot's pose, but planning a path through it is a different story.

  • One collision check is easy. Given angles (θ1,θ2)(\theta_1, \theta_2), determining whether the arm intersects an obstacle takes time proportional to the complexity of the shapes.
  • The space itself is continuous. With dd degrees of freedom and nn obstacles, the forbidden zones carve a dd-dimensional region whose topology can be arbitrarily complicated — an exponential number of connected components in the worst case.
  • The general problem is PSPACE-hard. John Reif proved in 1979 that the piano-movers problem — "does a free rigid body have a collision-free path between two configurations?" — is PSPACE-hard when the number of degrees of freedom is part of the input. This places it above NP in the complexity hierarchy; even a polynomial-size witness is not enough to verify a solution efficiently.
  • Practical planners trade exactness for speed. Algorithms like RRT (Rapidly-exploring Random Trees) and PRM (Probabilistic Roadmap Method) sample random configurations and connect them, finding paths probabilistically rather than exactly. They are complete only in the limit of infinite samples — a practical concession to intractability.

The lesson is sharp: C-space turns the planning question into a pure geometry problem, but geometry in high dimensions is hard. A robot with 6 joints lives in a 6-D C-space; a humanoid with 30 joints inhabits a space so vast that exact planning is never used in practice.

Where It Matters

The C-space abstraction shows up wherever a system with multiple degrees of freedom must avoid forbidden states:

  • Industrial robot arms: welding, assembly and painting robots all use C-space planners to thread arms through cluttered fixtures without collision.
  • Autonomous vehicles: a car has a 3-D configuration (x, y, heading); planning around pedestrians and parked cars is C-space navigation with non-holonomic constraints.
  • Surgical robotics: minimally-invasive robots must navigate tight anatomical corridors; the configuration space encodes every pose that avoids cutting the wrong tissue.
  • Protein folding: the dihedral angles along a peptide backbone define a high-dimensional C-space; finding the low-energy fold is path planning in that space toward an energy minimum.
  • Video-game AI and virtual production: character animation uses motion-planning ideas from C-space to navigate crowds and produce natural-looking movement.

Understand C-space and you have the shared language of motion planning and a glimpse of why protein folding is so computationally expensive.

Conclusion

Configuration space is one of the most elegant ideas in robotics: instead of reasoning about a complicated body sweeping through physical space, you shrink the robot to a point and let the obstacles grow to match. Path planning becomes point navigation.

But elegance does not mean easy. The forbidden zones in a high-dimensional C-space can form structures of extraordinary complexity, and planning exactly through them is PSPACE-hard. The practical world answers with probabilistic samplers — RRT, PRM and their descendants — that trade guarantees for speed, accepting that finding the optimal path may be forever out of reach.

The next time you watch a robot arm reach gracefully around an obstacle, remember: behind that movement is a point threading its way through a maze in a space you cannot see — a space where every dimension is an angle and every wall is a collision waiting to happen.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/configuration-space/Content licensed under CC BY-NC 4.0.