从 MJCF 到 CAD 装配体:Microduck 复刻项目如何还原整机结构

2026-09-01 71 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:11 分钟

Microduck 开源不到一周,社区就完成了一次相当彻底的逆向整理:开发者 fanhao375 从官方 MJCF 仿真模型和 47 个 STL 网格文件出发,还原出这台 25 厘米双足鸭子机器人的装配关系、爆炸图与 CAD 装配体,并整理了一份精确到 0.1 毫米的零件对照表。项目 README 给出的判断很直接:整机可以复刻。

这件事的价值不只在于“又多了一套 CAD 文件”。它展示了一条值得复用的机器人开源工作流:把面向仿真的模型资产,转化为可检查、可装配、可制造的工程资料。

MJCF 提供关系,STL 提供形状

MJCF 常用于描述机器人在仿真环境中的结构与动力学信息。一个典型模型会包含刚体层级、关节、坐标变换、网格引用,以及质量或碰撞相关参数。STL 则主要保存三角网格几何,本身通常不表达“哪个零件连接哪个零件”。

因此,从 MJCF 和 STL 恢复 CAD 装配体,并不是简单地批量导入 47 个文件。真正需要处理的是三类信息:

  • 零件身份:MJCF 中的 mesh 名称究竟对应哪个 STL 文件。
  • 装配层级:机身、髋部、腿部、足部等刚体如何形成父子关系。
  • 坐标变换:每个零件相对于父级的位置、姿态和旋转基准是什么。

可以把 MJCF 看作装配关系的“骨架”,把 STL 看作附着在骨架上的“外形”。只有同时解释两者,爆炸图和 CAD 装配体才不会沦为一堆摆放得大致相似的网格。

这也是零件对照表重要的原因。精确到 0.1 毫米,不只是为了让表格显得完整,而是为了发现单位换算、缩放比例、左右件镜像和装配间隙等问题。某个尺寸如果整体偏差 10 倍或 1000 倍,通常意味着毫米与米的单位没有统一;如果只有一侧偏差,则可能是镜像或坐标轴方向处理错误。

从“仿真能跑”到“实物能装”还有距离

仿真模型和制造模型服务于不同目标。仿真关注运动链、碰撞、质量和计算效率,制造则需要孔位、公差、紧固方式、材料厚度、线束空间和加工可行性。

STL 是三角网格,不等于带参数特征的 CAD 原生零件。将 STL 放入装配体,可以验证外形和相对位置,却未必能直接修改孔径、壁厚或倒角。要得到适合生产的模型,通常还需要重新建模关键机械特征,或者至少检查网格是否封闭、法向是否一致,以及是否存在自相交和破面。

“整机可复刻”也不应理解为下载文件后即可无条件量产。复刻者仍需核实:

  • 舵机、电机、轴承和紧固件的真实规格;
  • 关节零位、限位与左右腿对称关系;
  • 打印或加工后的收缩、间隙和强度;
  • 控制器、供电、线束与散热空间;
  • 仿真惯性参数是否与实际材料和器件一致。

换句话说,CAD 装配体解决了“零件如何组合”的核心问题,但物料采购、加工公差和机电联调仍然决定实物能否稳定行走。

实践:先自动核对 MJCF 与 STL 清单

在进入 CAD 软件之前,可以先用脚本检查 MJCF 引用了哪些网格、文件是否存在,以及 STL 的包围盒尺寸是否明显异常。下面的脚本只依赖 Python 标准库,支持二进制和 ASCII STL。

运行前,把 MODEL_XMLMESH_DIR 改成仓库中的实际路径。如果 MJCF 使用了编译器级别的 mesh 目录,还需要让 MESH_DIR 指向解析后的真实目录。

#!/usr/bin/env python3
import struct
import xml.etree.ElementTree as ET
from pathlib import Path

MODEL_XML = Path("model/microduck.xml")
MESH_DIR = Path("meshes")


def stl_vertices(path: Path):
    data = path.read_bytes()

    # Binary STL: 80-byte header, uint32 triangle count, 50 bytes per triangle.
    if len(data) >= 84:
        count = struct.unpack_from("<I", data, 80)[0]
        if 84 + count * 50 == len(data):
            for index in range(count):
                offset = 84 + index * 50 + 12
                for vertex in range(3):
                    yield struct.unpack_from("<fff", data, offset + vertex * 12)
            return

    # Fall back to ASCII STL.
    for line in data.decode("utf-8", errors="ignore").splitlines():
        fields = line.strip().split()
        if len(fields) == 4 and fields[0].lower() == "vertex":
            yield tuple(map(float, fields[1:]))


def bounds(path: Path):
    vertices = list(stl_vertices(path))
    if not vertices:
        raise ValueError("no vertices found")
    minimum = [min(v[axis] for v in vertices) for axis in range(3)]
    maximum = [max(v[axis] for v in vertices) for axis in range(3)]
    return [maximum[axis] - minimum[axis] for axis in range(3)]


tree = ET.parse(MODEL_XML)
assets = {}
for mesh in tree.findall(".//asset/mesh"):
    name = mesh.get("name")
    filename = mesh.get("file")
    if name and filename:
        assets[name] = filename

used_meshes = {
    geom.get("mesh")
    for geom in tree.findall(".//geom[@mesh]")
    if geom.get("mesh")
}

print(f"Declared meshes: {len(assets)}")
print(f"Referenced meshes: {len(used_meshes)}")

for name in sorted(used_meshes):
    filename = assets.get(name)
    if not filename:
        print(f"MISSING DECLARATION  {name}")
        continue

    path = MESH_DIR / filename
    if not path.exists():
        print(f"MISSING FILE         {name}: {path}")
        continue

    try:
        size = bounds(path)
        print(
            f"OK {name:28} "
            f"{size[0]:9.1f} x {size[1]:9.1f} x {size[2]:9.1f}  {path}"
        )
    except ValueError as error:
        print(f"INVALID STL          {name}: {error}")

unused = sorted(set(assets) - used_meshes)
for name in unused:
    print(f"UNUSED ASSET         {name}: {assets[name]}")

执行方式如下:

python3 inspect_microduck_assets.py

输出尺寸使用 STL 文件自身的单位。如果仓库约定 STL 以毫米建模,可以直接按毫米阅读;如果 MJCF 通过 scale 属性缩放网格,还必须将缩放值计入最终尺寸。这个脚本适合发现缺失文件、未使用资产和数量级异常,但不能替代 CAD 中的干涉检查。

还可以用命令快速确认仓库里的 STL 数量:

find . -type f -iname '*.stl' -print | sort | tee /tmp/microduck-stl.txt
wc -l /tmp/microduck-stl.txt

如果数量与预期的 47 个不一致,应先检查是否存在重复导出、不同版本目录或遗漏文件,再开始建立装配体。

建立可维护的 CAD 装配体

实际导入 CAD 时,可以按 MJCF 的 body 层级组织组件,而不是把所有 STL 放在同一层。一个可操作的流程是:

  1. 解析 MJCF 中的 body、joint、geom 和 mesh 引用,生成零件映射表。
  2. 统一米、毫米以及网格 scale,记录所有隐式换算。
  3. 先建立躯干到髋关节的主链,再分别装配左右腿。
  4. 用关节轴和零位验证旋转方向,不只依靠外观对齐。
  5. 在几个典型姿态下检查零件干涉和运动范围。
  6. 为电机、轴承、螺钉等标准件补充型号与采购信息。
  7. 对需要加工的关键零件重建参数化特征,并标注公差。

爆炸图适合检查是否漏件,完整装配体适合检查空间关系,而零件对照表负责把仿真命名、文件名、CAD 名称和制造编号连接起来。三者配合,才能让后来者理解并修改模型,而不是只能重复作者的导入步骤。

采用这套成果前应检查什么

Microduck 的社区复刻说明,一套公开的仿真资产已经可能包含足够多的机械结构信息。MJCF 保存运动链和坐标关系,STL 保存外形,细致的映射和测量则将两者重新组织成工程装配资料。

准备动手复刻时,建议把工作分成三个验收阶段:先验证文件完整性和单位,再验证关节、零位及干涉,最后才制作实体样件。第一轮加工优先选择少量关键连接件和单腿子装配,确认孔位、公差与执行器匹配后再扩展到整机。这样既能利用现有 CAD 成果,也能避免把仿真模型中的简化假设直接带到实体机器人上。


相关推荐