简介:DH2URDF 是一份面向机器人学与运动学建模学习者的 JavaScript 工具资源,用于将 Denavit-Hartenberg 参数表转换为统一机器人描述格式(URDF),解决手工编写 URDF 繁琐、易错的问题。资源包共 9 个文件,以 4 个 js 脚本为核心,辅以 1 个 html 演示页面、1 个 urdf 示例模型、1 个 litcoffee 源码及 license、md 说明文档,整体约 138KB,结构轻量,便于直接运行与二次开发。转换结果会将各轴以 URDF 圆柱体形式放置在 DH 参考系上,读者可据此逐步搭建完整机器人模型,并在配套演示页面中查看 URDF 效果、调整关节值进行验证。目前已有 1049 人学习下载,适合需要快速生成机器人模型、理解 DH 参数与 URDF 映射关系的中级学习者参考使用。
1. 从一张 DH 参数表到能在仿真里动起来的机器人:DH2URDF 到底解决了什么
如果你手头有一台串联机械臂的运动学参数,大概率是以 Denavit-Hartenberg 表的形式存在的——四个参数(连杆长度 a、连杆扭角 alpha、关节偏置 d、关节角 theta)排成一张表,每一行对应一个连杆。这张表在推导正逆运动学时非常顺手,但一旦你想把它丢进 Gazebo、RViz、PyBullet 或者任何基于 URDF 的仿真环境里,问题就来了:URDF 不认 DH 参数,它只认 link 和 joint 的层级关系、origin 的 xyz/rpy、以及 axis 向量。DH2URDF 就是干这件事的——把 DH 表自动转成 URDF 的 link/joint 结构。它用 JavaScript 写成,意味着你可以在浏览器里跑,也可以塞进 Node.js 脚本里做批量转换。适合谁?手上有 DH 表、想快速验证运动学模型、又不想手动算每个 joint 的 origin 变换的工程师。手动算六个关节的 rpy?我试过,第三行就开始怀疑人生。
2. DH 参数到 URDF 的映射原理:为什么不能直接抄表
2.1 DH 表的两种约定与 URDF 的坐标系差异
DH 参数有两种主流约定:标准 DH(Standard DH)和改进 DH(Modified DH)。两者的区别在于坐标系附着的位置不同——标准 DH 把坐标系固定在连杆的远端,改进 DH 固定在近端。这个差异直接影响到相邻关节之间变换矩阵的乘法顺序。URDF 里的 joint origin 描述的是父 link 坐标系到子 link 坐标系的变换,而 DH 表描述的是相邻连杆坐标系之间的变换。如果你不先把约定对齐,转出来的 URDF 在仿真里要么关节轴方向反了,要么连杆直接飞到天上。
常见做法是:先确认你的 DH 表是哪种约定。标准 DH 的变换矩阵是 Rot_z(theta) * Trans_z(d) * Trans_x(a) * Rot_x(alpha),改进 DH 则是 Rot_x(alpha) * Trans_x(a) * Rot_z(theta) * Trans_z(d)。DH2URDF 这类工具通常默认处理标准 DH,但如果你拿的是改进 DH 的表,得先做一步转换或者手动调整参数顺序。
另一个容易翻车的地方是 URDF 的 joint axis。在 DH 约定里,关节旋转轴通常是 z 轴,但 URDF 里 axis 是定义在 joint 的父坐标系下的。如果你直接把 axis 写成 (0,0,1),但父坐标系的朝向和 DH 坐标系不一致,关节就会绕错误的轴转。我一般会先把每个 joint 的 origin 变换算出来,再反推 axis 应该是什么方向。
2.2 从 DH 参数计算 joint origin 的完整推导
假设你有一组标准 DH 参数,第 i 个关节的四个参数是 a_i、alpha_i、d_i、theta_i。从父 link 到子 link 的变换矩阵可以写成:
T_i = Rot_z(theta_i) * Trans_z(d_i) * Trans_x(a_i) * Rot_x(alpha_i)展开成 4x4 齐次变换矩阵后,URDF 的 joint origin 需要的是 xyz 和 rpy。xyz 直接从矩阵的平移部分取,rpy 需要从旋转矩阵反解。这里有个细节:URDF 的 rpy 是固定轴 XYZ 旋转顺序(即先绕 X 转 roll,再绕 Y 转 pitch,最后绕 Z 转 yaw),而 DH 变换里的旋转是绕 z 和 x 的复合。所以不能直接把 theta 和 alpha 填进去,必须做矩阵分解。
下面是一段用 JavaScript 计算 origin 的核心逻辑,我把它拆成了可复用的函数:
// 根据标准 DH 参数计算相邻坐标系的 4x4 变换矩阵 function dhTransform(a, alpha, d, theta) { const ct = Math.cos(theta), st = Math.sin(theta); const ca = Math.cos(alpha), sa = Math.sin(alpha); // 标准 DH 变换矩阵:Rot_z(theta) * Trans_z(d) * Trans_x(a) * Rot_x(alpha) return [ [ct, -st * ca, st * sa, a * ct], [st, ct * ca, -ct * sa, a * st], [0, sa, ca, d ], [0, 0, 0, 1 ] ]; } // 从旋转矩阵提取 URDF 的 rpy(固定轴 XYZ) function rotationToRPY(R) { // R 是 3x3 旋转矩阵 const pitch = Math.atan2(-R[2][0], Math.sqrt(R[0][0]**2 + R[1][0]**2)); let roll, yaw; if (Math.abs(Math.cos(pitch)) > 1e-6) { roll = Math.atan2(R[2][1], R[2][2]); yaw = Math.atan2(R[1][0], R[0][0]); } else { // 万向锁情况,取 roll=0 roll = 0; yaw = Math.atan2(-R[0][1], R[1][1]); } return [roll, pitch, yaw]; }这段代码里,dhTransform返回的是 4x4 矩阵,rotationToRPY负责把旋转部分转成 URDF 需要的 rpy 三元组。参数说明:a和d是长度量,单位要和 URDF 保持一致(通常用米);alpha和theta是弧度制,如果你手头的表是角度制,记得先转。万向锁的判断阈值我设的是 1e-6,实际用的时候可以根据精度要求调整。
2.3 生成 URDF 的 link/joint 骨架
有了每个关节的 origin 之后,URDF 的结构就很好搭了。每个连杆对应一个 link,每个关节对应一个 joint。joint 的 type 通常是 revolute(旋转关节)或 prismatic(移动关节),具体取决于你的机器人是哪种。对于旋转关节,axis 一般设成 (0,0,1),但前提是你的 DH 坐标系 z 轴和 URDF 父坐标系的 z 轴对齐——如果不齐,就得把 axis 写成变换后的方向。
// 根据 DH 表生成 URDF 字符串 function generateURDF(dhTable, robotName) { let urdf = `<robot name="${robotName}">\n`; // 创建 base link urdf += ` <link name="base_link"/>\n`; dhTable.forEach((row, i) => { const { a, alpha, d, theta } = row; const T = dhTransform(a, alpha, d, theta); const xyz = [T[0][3], T[1][3], T[2][3]]; const R = [ [T[0][0], T[0][1], T[0][2]], [T[1][0], T[1][1], T[1][2]], [T[2][0], T[2][1], T[2][2]] ]; const rpy = rotationToRPY(R); const parentLink = i === 0 ? 'base_link' : `link_${i}`; const childLink = `link_${i + 1}`; const jointName = `joint_${i + 1}`; urdf += ` <link name="${childLink}"/>\n`; urdf += ` <joint name="${jointName}" type="revolute">\n`; urdf += ` <parent link="${parentLink}"/>\n`; urdf += ` <child link="${childLink}"/>\n`; urdf += ` <origin xyz="${xyz.map(v => v.toFixed(6)).join(' ')}" `; urdf += `rpy="${rpy.map(v => v.toFixed(6)).join(' ')}"/>\n`; urdf += ` <axis xyz="0 0 1"/>\n`; urdf += ` <limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/>\n`; urdf += ` </joint>\n`; }); urdf += `</robot>`; return urdf; }这段代码的逻辑是:遍历 DH 表的每一行,算出变换矩阵,提取 xyz 和 rpy,然后拼成 URDF 的 XML 片段。注意几个参数:limit里的 lower/upper 是关节角度范围,effort 和 velocity 是力矩和速度限制,这些在纯运动学验证时可以随便填,但如果要跑动力学仿真,必须填真实值。toFixed(6)是为了避免浮点数精度导致的 XML 解析问题——我遇到过因为小数点后十几位导致 URDF 加载失败的情况,血泪经验。
3. 在 Node.js 里跑通转换:从 DH 表到 URDF 文件的完整流程
3.1 环境准备与依赖安装
DH2URDF 本身是一个 JavaScript 库,你可以在浏览器里直接引入,也可以在 Node.js 里用。我一般用 Node.js 做批量转换,因为可以写脚本自动化。先建一个空目录,初始化 npm:
mkdir dh2urdf-demo && cd dh2urdf-demo npm init -y如果你拿到的 DH2URDF 是一个 npm 包,直接npm install dh2urdf就行。如果是一个单文件脚本,把它放到目录里,用require或import引入。我手头这个版本是一个独立的dh2urdf.js,导出了generateURDF和dhTransform两个函数。为了验证,我还会装一个xml2js来检查生成的 URDF 是不是合法 XML:
npm install xml2js这一步不是必须的,但如果你要批量生成几十个 URDF,有个校验工具能省很多事。常见做法是:生成之后用xmllint或者xml2js解析一遍,确认没有标签闭合错误。
3.2 定义 DH 表并生成第一个 URDF
假设你有一台六轴机械臂,DH 参数如下表(单位:米和弧度):
| 关节 | a | alpha | d | theta |
|---|---|---|---|---|
| 1 | 0 | π/2 | 0.15 | 0 |
| 2 | 0.25 | 0 | 0 | 0 |
| 3 | 0.25 | 0 | 0 | 0 |
| 4 | 0 | π/2 | 0.15 | 0 |
| 5 | 0 | -π/2 | 0 | 0 |
| 6 | 0 | 0 | 0.1 | 0 |
把这组参数写成 JavaScript 数组,然后调用生成函数:
const { generateURDF } = require('./dh2urdf'); const dhTable = [ { a: 0, alpha: Math.PI / 2, d: 0.15, theta: 0 }, { a: 0.25, alpha: 0, d: 0, theta: 0 }, { a: 0.25, alpha: 0, d: 0, theta: 0 }, { a: 0, alpha: Math.PI / 2, d: 0.15, theta: 0 }, { a: 0, alpha: -Math.PI / 2, d: 0, theta: 0 }, { a: 0, alpha: 0, d: 0.1, theta: 0 } ]; const urdfString = generateURDF(dhTable, 'demo_arm'); console.log(urdfString);跑完之后,控制台会输出一段 URDF XML。把它保存成demo_arm.urdf,然后丢进 RViz 或者 PyBullet 里加载。如果模型显示正常,关节轴方向也对,那基本就成功了。参数说明:theta在 DH 表里通常是关节变量的初始值,对于旋转关节,这个值会被 URDF 的 joint state 覆盖,所以填 0 就行。alpha的正负号很关键,填反了会导致连杆扭曲方向错误。
3.3 用 xml2js 校验生成的 URDF
生成之后别急着往仿真里丢,先校验一下 XML 结构:
const xml2js = require('xml2js'); const parser = new xml2js.Parser(); parser.parseString(urdfString, (err, result) => { if (err) { console.error('URDF XML 解析失败:', err.message); return; } const robot = result.robot; console.log('机器人名称:', robot.$.name); console.log('link 数量:', robot.link.length); console.log('joint 数量:', robot.joint.length); // 检查每个 joint 是否有 parent 和 child robot.joint.forEach((j, i) => { if (!j.parent || !j.child) { console.warn(`joint_${i + 1} 缺少 parent 或 child`); } }); });这段代码会输出 link 和 joint 的数量,以及检查每个 joint 是否完整。如果 link 数量是 joint 数量加一,说明结构没问题。如果某个 joint 缺了 parent 或 child,URDF 加载时会直接报错。我一般还会检查 origin 的 xyz 和 rpy 是不是都是数字,有时候因为 DH 表里某个参数是字符串,算出来变成 NaN,XML 里就会出现xyz="NaN NaN NaN",这种错误在仿真里很难排查。
4. 避坑与排查:DH2URDF 转换中最容易翻车的五个地方
4.1 现象:URDF 加载后机器人所有连杆叠在一起
原因:DH 表的 theta 列填的是角度值而不是弧度值。比如你把 90 填进去,JavaScript 的 Math.cos(90) 算出来是 -0.448,而不是 0。解决:在传入 DH2URDF 之前,把所有角度统一转成弧度。我一般会在脚本开头加一个转换函数:
function degToRad(deg) { return deg * Math.PI / 180; } // 如果 DH 表是角度制,先 map 一遍 const dhTableRad = dhTableDeg.map(row => ({ ...row, alpha: degToRad(row.alpha), theta: degToRad(row.theta) }));4.2 现象:关节旋转轴方向反了,正转变成反转
原因:DH 约定里的 z 轴方向和 URDF 父坐标系的 z 轴方向不一致。标准 DH 里关节轴是 z 轴,但经过 alpha 旋转后,子坐标系的 z 轴可能和父坐标系不同向。解决:不要无脑写axis xyz="0 0 1",而是根据变换矩阵的第三列(即子坐标系 z 轴在父坐标系下的方向)来设置 axis。如果第三列是 (0,0,-1),那 axis 就得写成0 0 -1。
4.3 现象:RViz 里模型显示正常,但 PyBullet 里关节无法驱动
原因:URDF 的 joint type 写成了 fixed,或者 limit 标签缺失。PyBullet 对 URDF 的容错性比 RViz 低,如果 joint 没有 limit,它会默认当成 fixed 处理。解决:确保每个 revolute joint 都有<limit>标签,并且 lower/upper 不是 NaN。另外,PyBullet 要求 joint 的 axis 必须是单位向量,如果你写了0 0 2,它会报错。
4.4 现象:生成的 URDF 在浏览器里能解析,在 ROS 里报 XML 格式错误
原因:JavaScript 的toFixed在某些情况下会输出科学计数法,比如1e-7,而 URDF 的 XML 解析器不认这种格式。解决:在拼接字符串之前,用一个格式化函数把极小值归零:
function formatNum(v) { if (Math.abs(v) < 1e-10) return '0'; return v.toFixed(6); }4.5 现象:多关节机器人转换后,末端执行器位置和预期差很多
原因:DH 表的参数顺序搞混了。标准 DH 的四个参数顺序是 a、alpha、d、theta,但有些教材写成 theta、d、a、alpha。如果你按错误的顺序传参,算出来的变换矩阵完全不对。解决:在代码里把参数名写清楚,不要用位置参数。我一般会用对象解构,像{ a, alpha, d, theta }这样,谁也别想搞混。
5. 进阶:批量转换与参数化验证
当你手头有十几组 DH 参数需要批量转成 URDF 时,一个个手动跑就太慢了。我一般会写一个批处理脚本,从 JSON 文件里读 DH 表,生成对应的 URDF 文件,然后自动做一轮正运动学验证。
const fs = require('fs'); const path = require('path'); const { generateURDF, dhTransform } = require('./dh2urdf'); // 读取包含多组 DH 表的 JSON 文件 const configs = JSON.parse(fs.readFileSync('dh_configs.json', 'utf8')); configs.forEach((config, idx) => { const urdf = generateURDF(config.dhTable, config.name); const outPath = path.join(__dirname, 'output', `${config.name}.urdf`); fs.writeFileSync(outPath, urdf); console.log(`已生成: ${outPath}`); // 正运动学验证:计算末端位置 let T = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]; config.dhTable.forEach(row => { const Ti = dhTransform(row.a, row.alpha, row.d, row.theta); // 矩阵乘法 T = T * Ti T = multiplyMatrix(T, Ti); }); const endPos = [T[0][3], T[1][3], T[2][3]]; console.log(` ${config.name} 末端位置: [${endPos.map(v => v.toFixed(4)).join(', ')}]`); }); function multiplyMatrix(A, B) { const C = Array.from({ length: 4 }, () => new Array(4).fill(0)); for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) for (let k = 0; k < 4; k++) C[i][j] += A[i][k] * B[k][j]; return C; }这段脚本做了两件事:生成 URDF 文件,以及用 DH 参数直接算一遍正运动学,输出末端位置。为什么要算这个?因为如果你把生成的 URDF 丢进仿真,仿真里的末端位置应该和这里算出来的一致。如果不一致,说明 URDF 的 origin 或者 axis 有问题。我一般会把这个末端位置和仿真里读到的末端 link 位置做对比,误差在 1e-4 以内就算通过。
参数说明:dh_configs.json的结构是[{ name: "arm1", dhTable: [...] }, ...],每个 dhTable 里的参数都是弧度制。multiplyMatrix是标准的 4x4 矩阵乘法,没有用任何库,纯手写,方便你直接抄。如果你要处理更复杂的验证,比如带关节角度的正运动学,可以把 theta 换成变量,循环几组角度值,看末端轨迹是不是连续。
还有一个技巧:生成的 URDF 里,link 的 inertial 和 visual/collision 都是空的。如果你只是做运动学验证,这没问题;但如果要跑动力学仿真,必须补上 mass、inertia 和几何形状。我一般会写一个后处理脚本,给每个 link 加上默认的质量和惯性矩阵,至少让仿真能跑起来。质量可以随便填个 1kg,惯性矩阵用单位矩阵乘以一个小系数,等模型验证通过后再换成真实值。
从那以后我每次做 DH 到 URDF 的转换,都会先跑一遍正运动学对比,确认末端位置一致了再往仿真里丢。这个习惯帮我省了至少三次通宵排查的时间。希望帮到你。
本文还有配套的精品资源,点击获取