Three.js 如何显示坐标系、碰撞体和质心

前面几篇讲交互。这一篇回到模型本身 —— URDF 里除了 <visual> 描述的外观,还有三样渲染用不到、调模型时离不开的东西:关节坐标系、碰撞体、<inertial>。

它们默认都不显示,需要时再打开。

打开碰撞体和质心后的模型:红色是碰撞几何,绿球是各 link 的质心,左上角按 link 单独开关

1. 关节坐标系:一个三色十字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { BufferGeometry, Float32BufferAttribute, LineBasicMaterial, LineSegments } from "three";

// 关节坐标系辅助对象
class JointAxesHelper extends LineSegments {
constructor( size = 1, material = new LineBasicMaterial( { vertexColors: true, toneMapped: false } ) ) {
// 顶点
const vertices = [
0, 0, 0, size, 0, 0, // x 轴
0, 0, 0, 0, size, 0, // y 轴
0, 0, 0, 0, 0, size // z 轴
];
// 顶点颜色
const colors = [
1, 0, 0, 1, 0.6, 0, // x 轴渐变色
0, 1, 0, 0.6, 1, 0, // y 轴渐变色
0, 0, 1, 0, 0.6, 1 // z 轴渐变色
];

const geometry = new BufferGeometry();
geometry.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) );
geometry.setAttribute( 'color', new Float32BufferAttribute( colors, 3 ) );

super( geometry, material );
}

// 清理缓存
dispose() {
this.geometry.dispose();
('dispose' in this.material) && this.material.dispose();
}
}

export { JointAxesHelper };

几条值得注意的:

  • 继承 LineSegments,每两个顶点画一段。所以每根轴写了两次 0, 0, 0 —— 线段不共享起点,没有索引可用。
  • 颜色写在顶点上,靠材质的 vertexColors: true 生效。每根轴从纯色渐变到浅色(红→橙、绿→黄绿、蓝→浅蓝),比单色更容易分清方向。
  • dispose() 成对释放:geometry 和 material 都要,和上次讲高亮材质是同一个道理 —— 不释放就是泄漏。

用的时候在解析 joint 的过程中挂上去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const axesMat = new LineBasicMaterial({
vertexColors: true,
toneMapped: false,
depthTest: false,
depthWrite: false,
fog: false,
});
const jointAxisHelper = new JointAxesHelper(0.2, axesMat);
jointAxisHelper.visible = false;
Object.assign(jointAxisHelper.userData, {
isURDFHelper: true,
helperType: "jointAxisHelper",
});
jointObj.add(jointAxisHelper);
jointAxisMap.set(jointName, jointAxisHelper);

材质上那五个参数各管一件事:

参数 作用
vertexColors: true 用顶点色,而不是材质自己的颜色
toneMapped: false 不被色调映射改色,红就是正红
depthTest: false + depthWrite: false 穿透模型显示
fog: false 不受雾影响

depthTest: false 是关键一条。不关掉的话,轴会被大腿、外壳挡住,只剩下露在外面的一小截。关掉之后整个十字永远画在模型上面 —— 代价是失去前后关系,看不见的那半截也画出来。

另外两行看着不起眼,其实是配套的:

  • visible = false —— 默认隐藏。坐标系全部打开会很乱,所以按名字存进 jointAxisMap,想调试哪个再取出来打开。
  • userData.isURDFHelper = true —— 给射线拾取用的。拖动那篇的 traverseMeshInLink 遇到 isURDFHelper 就停止下钻,所以这个十字不会被选中。没有这个标记,鼠标扫过去会挑中辅助线而不是零件。

size 给 0.2(米)是轴长,比关节本身略大一点,够看清又不至于盖住模型。

2. 碰撞体:只可视化,不做碰撞检测

<collision> 描述”碰撞时算成什么”。Web 前端一般不会真拿它跑碰撞检测 —— 物理仿真在服务端或专门的引擎里做 —— 而是只把它画出来,用来检查简化模型摆得对不对。

读懂一个碰撞体,看两件事:形状尺寸,和 <origin> 把它放在哪。

项 值 含义
形状 圆柱 —
半径 0.02 m 直径 4 cm,比较细
长度 0.2 m 20 cm
origin xyz="0 0 -0.2" 圆柱中心相对 link 坐标系向下偏移 0.2 m
rpy 0 0 0 无旋转

有一个容易看错的地方:URDF 的圆柱沿自身 Z 轴延伸,origin 给的是中心,不是底面。 所以这个圆柱在 link 坐标系下覆盖的是 z 从 -0.3 到 -0.1(中心 -0.2,上下各 0.1),不是从 -0.2 往下 0.2。

大腿和小腿用的就是这根:它们的下一个关节在 z = -0.4,整段长约 0.4 m,而 0.2 m 的细圆柱大致盖住中下部。用简单几何体代替几万个三角形的外观 mesh,碰撞计算的开销小得多,代价是它只是个近似。

3. 质心:<inertial> 的 <origin>

<inertial> 里是三样:<mass>、<origin>、<inertia>。其中 origin 就是这个 link 的惯性坐标系原点,也就是质心。

注意 <link> 里三套 <origin> 各管各的,互不相干:<visual> 的管外观摆在哪儿,<collision> 的管碰撞体摆在哪儿,<inertial> 的管质心在哪儿。

看一个例子:mass = 4.962 kg,质心在 z = -0.1945。这个 link 长约 0.4 m,质心落在 -0.1945 说明质量集中在下半段(靠近膝关节那侧)—— 髋部电机加上大腿上段结构比较重,这个位置是合理的。

4. 惯性张量:六个数字怎么读

<inertia> 是一个 3×3 对称矩阵,只有六个独立分量,单位是 kg·m²:

参数 值 含义
ixx 4553.605E-06 绕 X 轴的转动惯量
iyy 5688.730E-06 绕 Y 轴的转动惯量
izz 3548.907E-06 绕 Z 轴的转动惯量
ixy 0.005E-06 X-Y 惯性积(几乎为 0)
ixz 0.817E-06 X-Z 惯性积(几乎为 0)
iyz -345.157E-06 Y-Z 惯性积

换算成直观的数:Ixx ≈ 0.004554、Iyy ≈ 0.005689、Izz ≈ 0.003549、Iyz ≈ -0.000345,其余交叉项基本为 0。

  • 对角线(ixx / iyy / izz)是绕三个轴各自的转动惯量,数值越大,绕那根轴转起来越”沉”。
  • 交叉项(ixy / ixz / iyz)是惯性积,反映质量分布相对主轴偏了多少。

这组数里交叉项都接近 0,说明这个 link 的质量分布已经比较接近主轴对齐,没有明显偏斜;Iyy 最大,说明绕 Y 轴旋转时惯性最大。

可以用一句话记:质量告诉你”有多重”,惯性张量告诉你”转的时候有多难转”。 它不只跟质量有关,还跟质量离旋转轴的分布有关 —— 同样是 1 kg,摊成一根长杆就比缩成一个球难转得多,这就是”距离平方加权”的意思。

这些数是给物理引擎算力矩用的,渲染完全用不到。但画出来是有价值的:质心位置和惯量分布合不合理,一眼能看出来。

参考

Three.js 如何显示坐标系、碰撞体和质心

https://wywzixin.github.io/threejs/urdf-helpers/

作者

wangyawei

发布于

2026-09-18

更新于

2026-09-18

许可协议