Denavit-Hartenberg parameters
A serial robot arm is a stack of joints, each one carrying the next. To compute where the gripper ends up, you need to describe how every joint sits relative to the one before it, in full 3D.
Denavit-Hartenberg parameters are the standard answer. Four numbers per joint, the same four for every robot ever built, arranged in a table with one row per joint. That table is a complete kinematic description. Nothing else is needed to find where the end effector is.
The tool below runs in that direction. Type a table, get the arm.
Serial-link manipulator / DH parameters
Type a DH table, get a robot
Fill in one row per joint and the arm below is built from it. Add joints, delete them, switch a revolute for a prismatic, and drag the sliders to drive whichever parameter that joint owns. Drag the view to orbit.
| Joint | Type | a | alpha | d | theta | Remove |
|---|
Lengths in millimetres, angles in degrees. The starred cell is the one that joint's motor drives.
Add joints, delete them, change a number, switch a revolute for a prismatic. The arm rebuilds from whatever the table currently says.
The four columns
Each row turns into one transform, built from four elementary moves applied in a fixed order:
Order matters, and this order is the convention:
- rotates about the current z-axis.
- slides along that same z-axis.
- slides along the new x-axis, the one the rotation just produced.
- rotates about that new x-axis.
Two of these are translations and two are rotations, and they alternate between the z-axis and the x-axis. That alternation is the whole trick: no parameter ever refers to the y-axis, because the x and z moves are enough to reach any frame that a well-chosen joint axis assignment produces.
Turn on Split d and a and the two translations draw as separate segments. The blue one is running along z, the dark one is running along x, and they always meet at a right angle. A robot drawing shows you the final dogleg; the table shows you the two moves that produced it.
Which number the motor drives
Three of the four numbers are fixed geometry, machined into the link. One is the joint variable, and it is the only thing that changes while the robot moves.
For a revolute joint the variable is . For a prismatic joint it is . That is the entire difference between the two joint types in this representation. Switch a row’s type in the table and watch the star move from one column to the other, and the slider below change with it.
This is why DH is worth the trouble. A six-axis arm with a mixed set of revolute and prismatic joints still reduces to six numbers that vary and eighteen that do not.
Alpha is the one that leaves the plane
Load the Planar 3R preset. Every is zero, so every joint axis stays parallel to the base axis. Drive the joints anywhere you like and the arm stays in one plane forever. It cannot do otherwise.
Now change a single to 90 and the arm lifts out of that plane.
Nothing about the twisted joint moved. Its own origin sits exactly where it did, because rotates about the x-axis that passes through it. What changed is the direction of the next joint’s axis, and therefore the plane every downstream link sweeps through.
is a design decision, not a pose. It describes how the next joint is welded into the structure. When someone says a robot is “an anthropomorphic arm” or “a SCARA”, they are describing a pattern of twists in this column.
Three tables, three robot classes
The presets are worth stepping through, because the differences between whole categories of industrial robot live in a handful of cells.
Planar 3R has every twist at zero. Three parallel axes, one plane, and the arm from the two-link IK demo with an extra joint. Flat robots are the ones where this column is empty.
SCARA has two parallel revolutes, then a prismatic riding straight down, then a final rotation. Look at joint 2: . That flips the z-axis over so the prismatic joint extends downward into the workspace instead of up into the ceiling. One cell decides which way the machine reaches.
Articulated 6R alternates twists through the last three joints. That pattern puts three axes through a single point, which is a spherical wrist, and it is the reason this arm class has a closed-form inverse kinematics solution at all. The structure that makes the math tractable is visible right there in the table.
The convention trap
There are two definitions of these four parameters in circulation.
Classic DH, the original, is what this tool uses and what is written above. Modified DH, from Craig’s textbook, applies the same four moves in a different order, which attaches the frames one link further along.
Both are correct. Both are common. They use identical symbols for numbers that are not the same, and a table written in one convention, loaded into software expecting the other, does not throw an error. It produces an arm that is confidently, quietly wrong, off by exactly one link of geometry.
If you take a DH table from a datasheet or a paper, find out which convention it uses before you trust it. If you publish one, say which you used. That sentence has saved more debugging hours than any clever algorithm.
The code
The same chain in C++, with the convention it implements written at the top, because that is the one thing a reader cannot recover from the code by eye:
// Classic Denavit-Hartenberg, the convention written above:
//
// T_i = Rot_z(theta_i) * Trans_z(d_i) * Trans_x(a_i) * Rot_x(alpha_i)
//
// Not Craig's modified convention, which applies the same four moves in
// a different order and hangs the frames one link further along. A
// modified-DH table fed to this code compiles, runs, and returns a pose
// that is wrong by exactly one link of geometry.
using Mat4 = std::array<std::array<double, 4>, 4>; // row-major
struct Joint {
// Three of these are fixed geometry. The fourth is what the motor
// drives: theta on a revolute joint, d on a prismatic one.
double a, alpha, d, theta;
};
Mat4 identity() {
Mat4 m{};
for (int i = 0; i < 4; i++) m[i][i] = 1.0;
return m;
}
// Order matters: applies b first, then a.
Mat4 multiply(const Mat4& a, const Mat4& b) {
Mat4 out{};
for (int row = 0; row < 4; row++)
for (int col = 0; col < 4; col++)
for (int k = 0; k < 4; k++) out[row][col] += a[row][k] * b[k][col];
return out;
}
Mat4 rotZ(double theta) {
const double c = std::cos(theta), s = std::sin(theta);
return {{{c, -s, 0, 0},
{s, c, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}}};
}
Mat4 rotX(double alpha) {
const double c = std::cos(alpha), s = std::sin(alpha);
return {{{1, 0, 0, 0},
{0, c, -s, 0},
{0, s, c, 0},
{0, 0, 0, 1}}};
}
Mat4 transZ(double d) {
return {{{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, d},
{0, 0, 0, 1}}};
}
Mat4 transX(double a) {
return {{{1, 0, 0, a},
{0, 1, 0, 0},
{0, 0, 1, 0},
{0, 0, 0, 1}}};
}
// One row of the table, one transform, built from its four elementary
// parts rather than a hand-typed closed form. Every piece here is
// trivially correct by construction, which is worth more than a formula
// that is merely well remembered. It is also where a convention swap
// would go: modified DH multiplies these same four in the order
// Rot_x, Trans_x, Rot_z, Trans_z.
Mat4 dhTransform(const Joint& j) {
return multiply(multiply(multiply(rotZ(j.theta), transZ(j.d)), transX(j.a)),
rotX(j.alpha));
}
// Chain down the rows. Each frame's pose in the base frame, base
// included as frame 0, so the end effector is frames.back() and there
// are joints.size() + 1 of them.
std::vector<Mat4> forwardKinematics(const std::vector<Joint>& joints,
const Mat4& base = identity()) {
std::vector<Mat4> frames{base};
Mat4 t = base;
for (const Joint& joint : joints) {
t = multiply(t, dhTransform(joint));
frames.push_back(t);
}
return frames;
}
// A frame's origin in the base frame is its translation column. The
// rest of the matrix is the orientation, and a gripper needs both.
struct Vec3 { double x, y, z; };
Vec3 origin(const Mat4& t) { return {t[0][3], t[1][3], t[2][3]}; }
Where this stops
Forward kinematics is the easy direction, and a DH table gives it to you as a chain of matrix multiplications, one per row, with no special cases.
Going backwards, from a desired gripper pose to the joint values that achieve it, is a different problem entirely. Sometimes there is a closed-form answer, which is exactly why the spherical wrist above is so common. Sometimes there is no solution, or infinitely many. The planar two-link arm has two solutions for most points and none for the rest, and that is the simplest interesting case there is.
The table gets you the arm. Getting the arm where you want it is the next problem.