URDF (doublePendulum 시뮬레이션 예제)

mmmn0616·2026년 8월 23일

직립 상태의 더블 펜듈럼

0. 배경

ROS2(Humble) + Gazebo(Harmonic)로 더블 펜듈럼을 세우는 프로젝트를 진행하면서
Phase 1에서 URDF/Xacro로 로봇 모델을 직접 작성했다. 이와 관련하여 URDF의 핵심 개념을 정리해본다.


1. URDF란

Unified Robot Description Format. 로봇의 기구학(kinematic)
구조를 XML로 기술하는 표준 포맷이다.

<robot name="my_robot">
  ...
</robot>

<robot> 태그는 URDF 파일 전체를 감싸는 최상위 루트 요소다. 이 태그
안에 로봇을 이루는 모든 <link>와 <joint>가 들어간다.

URDF에서 로봇은 link(강체)와 joint(관절)가 번갈아 이어진 그래프로
표현된다. 이 그래프는 반드시 트리(tree) 구조여야 한다. 즉 닫힌
루프(closed kinematic chain)를 표현할 수 없다.

URDF link-joint 트리 구조 예시
출처: ROS Wiki - urdf/Tutorials/Create your own urdf file

박스(link1~link4)가 강체, 파란 타원(joint1~joint3)이 관절이다. link1
하나에서 joint1/joint2 두 갈래로 뻗어나가고, 각 갈래가 다시 한 줄로 이어지는
트리 구조이다. 한 link가 여러 자식을 가질 수는 있지만(link1처럼),
자식이 부모 여러 개를 갖거나 경로가 다시 합쳐지는 경우는 없다.

하나의 강체(rigid body)는 보통 3가지 요소로 기술한다.

<link name="LINK_NAME">
  <visual>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <geometry><!-- box / cylinder / sphere / mesh --></geometry>
    <material name="..."/>
  </visual>
  <collision>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <geometry><!-- 보통 visual보다 단순한 형상 --></geometry>
  </collision>
  <inertial>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <mass value="..."/>
    <inertia ixx="..." ixy="..." ixz="..." iyy="..." iyz="..." izz="..."/>
  </inertial>
</link>
  • <visual>: 화면에 렌더링되는 형상. geometry(box/cylinder/sphere/mesh)
    • material(색상)로 구성
  • <collision>: 충돌 계산에 쓰이는 형상. 실제 로봇은 렌더링용 메시가
    정밀할수록 충돌 계산이 무거워지기 때문에, 복잡한 메시를 visual에 쓰더라도
    collision은 box/cylinder 같은 단순 도형으로 근사하는 게 일반적이다
  • <inertial>: mass, origin(무게중심 위치), <inertia>
    (관성텐서 ixx iyy izz ixy ixz iyz) — 물리 시뮬레이션에 필요한 질량
    속성

1-3. <joint> — 두 link를 연결하는 태그

<joint name="JOINT_NAME" type="revolute">
  <parent link="PARENT_LINK"/>
  <child link="CHILD_LINK"/>
  <origin xyz="0 0 0" rpy="0 0 0"/>
  <axis xyz="0 0 1"/>
  <limit lower="..." upper="..." effort="..." velocity="..."/>
  <dynamics damping="..." friction="..."/>
</joint>
  • <parent> / <child>: 이 조인트가 어떤 link를 어떤 link에
    연결하는지. 트리 구조를 만드는 핵심 필드
  • <origin>: parent link 좌표계 기준으로 이 조인트(= child link의
    원점)가 어디에 위치하는지
  • type: revolute(각도 제한 있음), continuous(제한 없이 무한 회전),
    prismatic(직동), fixed(고정), floating/planar(자유도 여러 개) 등
  • <axis>: 회전/이동 축 방향
  • <limit>: effort(최대 힘/토크), velocity(최대 속도) 등 동작
    제한
  • <dynamics>: damping(감쇠), friction(마찰) — 관절의 물리적
    특성

2. 이중 진자 모델 구성해보기

위 개념들을 개인 프로젝트(doublePendulum_sim)에
실제로 어떻게 적용되었는지를 정리한다(이중진자 모델). 2-link, fully actuated, revolute joint
두 개짜리 구조를 만들면서 xacro로 작성한 실제 코드를 그대로 가져왔다.

기둥 역할을 하는 base_link는 2장에서 본 <link> 구조를 그대로 채운
가장 단순한 형태다:

<link name="base_link">
  <visual>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <geometry><cylinder radius="0.06" length="0.06"/></geometry>
    <material name="base_grey"/>
  </visual>
  <collision>
    <origin xyz="0 0 0" rpy="0 0 0"/>
    <geometry><cylinder radius="0.06" length="0.06"/></geometry>
  </collision>
  <inertial>
    <mass value="1.0"/>
    <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/>
  </inertial>
</link>

앞서 설명했듯, <visual>에서 시각화, <collision>에서 실제 물리 엔진이 관여하는 모양 정의, <inertial>에서 질량과 관성 텐서가 정의된 것을 볼 수 있다.

이 이중 진자 모델에서 실제로 움직이는 부분인 link1/link2는 매크로로 정의한 균일 막대(uniform rod)다 (매크로 설명은 3-3에서):

<xacro:macro name="rod_link" params="name mass length radius material_name">
  <link name="${name}">
    <visual>
      <origin xyz="0 0 ${length/2}" rpy="0 0 0"/>
      <geometry><cylinder radius="${radius}" length="${length}"/></geometry>
      <material name="${material_name}"/>
    </visual>
    <collision>
      <origin xyz="0 0 ${length/2}" rpy="0 0 0"/>
      <geometry><cylinder radius="${radius}" length="${length}"/></geometry>
    </collision>
    <inertial>
      <origin xyz="0 0 ${length/2}" rpy="0 0 0"/>
      <mass value="${mass}"/>
      <inertia
        ixx="${mass*(3*radius*radius+length*length)/12}" ixy="0" ixz="0"
        iyy="${mass*(3*radius*radius+length*length)/12}" iyz="0"
        izz="${mass*radius*radius/2}"/>
    </inertial>
  </link>
</xacro:macro>

link1/link2의 <inertia> 값은 물리 공식으로 직접 계산해서 넣었다.

관성은 무게중심 기준으로 줘야하기 때문에 <inertial><origin>을 length/2만큼 띄워서 COM 위치를 명시했다.

cylinder의 관성 공식은:

Ixx=Iyy=m(3r2+L212),Izz=mr22I_{xx} = I_{yy} = m\left(\frac{3r^2 + L^2}{12}\right), \quad I_{zz} = \frac{m r^2}{2}

가늘고 긴 막대(r≪Lr \ll L)로 근사하면 Ixx≈112mL2I_{xx} \approx \frac{1}{12}mL^2 로, 균일 막대 공식이 된다.
실제 값 대입(m=1.0kg, L=1.0m,r=0.02m):

ixx = iyy = 1.0 * (3*0.02² + 1.0²) / 12 ≈ 0.0834
izz         = 1.0 * 0.02² / 2         ≈ 0.0002

2-2. 관절 정의 — joint1 / joint2

<joint name="joint1" type="revolute">
  <parent link="base_link"/>
  <child link="link1"/>
  <origin xyz="0 0 0" rpy="0 0 0"/>
  <axis xyz="0 1 0"/>
  <limit lower="-1000" upper="1000" effort="60.0" velocity="50.0"/>
  <dynamics damping="0.05" friction="0.0"/>
</joint>

관절 축(joints) 오버레이

  • type은 revolute: continuous(무한 회전)와 달리
    <limit>에 lower/upper 각도 제한을 반드시 명시해야 하는 회전관절
    타입. 이 프로젝트는 lower="-1000" upper="1000"으로 사실상 거의
    무제한에 가깝게 열어뒀지만, 그래도 숫자 자체는 명시해야 하는 게
    revolute와 continuous의 차이다.
  • <axis xyz="0 1 0">: Y축 회전 → X-Z 평면에서 스윙한다는 뜻. 위
    스크린샷의 초록색 화살표가 이 축 방향이다
  • <limit>: effort(최대 토크), velocity(최대 각속도) — 컨트롤러
    saturation과 바로 연결되는 값. 실제로 joint1은 tau1_max=60.0,
    joint2는 tau2_max=30.0으로 서로 다르게 줬다 (아래팔쪽이 더 가벼운
    토크로도 충분해서)
  • <dynamics damping="0.05">: 0으로 두면 시뮬레이션이 비현실적으로
    오래 진동한다

2-3. Xacro로 반복 줄이기

Xacro(XML Macro)는 URDF를 짤 때 반복되는 XML을 변수·매크로로 줄여주는 전처리기다. 순수 URDF는 값이 없는 정적 XML이라서, 같은 숫자를 여러 군데에 직접 박아 넣어야 하는 상황이 자주 생긴다. .urdf.xacro 파일을 xacro 명령으로 처리하면, 아래 나올 변수/매크로가 전부 실제 값으로 치환된 순수 .urdf(표준 XML)가 만들어진다.

<xacro:property name="m1" value="1.0"/>
<xacro:property name="m2" value="1.0"/>
<xacro:property name="L1" value="1.0"/>
<xacro:property name="L2" value="1.0"/>
<xacro:property name="rod_radius" value="0.02"/>
<xacro:property name="joint_damping1" value="0.05"/>
<xacro:property name="joint_damping2" value="0.05"/>
<xacro:property name="tau1_max" value="60.0"/>
<xacro:property name="tau2_max" value="30.0"/>
<xacro:property name="qd_max" value="50.0"/>

질량/길이부터 관절 감쇠, 토크·속도 제한까지 전부 상수로 뽑아뒀다. 이렇게 변수화해두면 나중에 막대 길이를 바꿔서 재실험하는 것과 같은 작업을 간단히 끝낼 수 있다.

macro를 쓰는 이유

3-1에서 본 것처럼 link 하나를 정의하려면 <visual>/<collision>/<inertial>을 다 채운 꽤 긴 <link> 블록이 필요하다. link1과 link2는 질량·길이·색깔만 다를 뿐, 그 안의 구조는 완전히 똑같다.

이렇게 반복적으로 나오는 코드는 매크로로 지정해놓으면 편리하다.

3-1에서 본 rod_link 매크로를 만들고, 이름/질량/길이/반지름/색깔 5개 값만 넣어 호출하면 매크로에서 정의해둔 <link> 블록 전체(visual + collision + inertial 다 포함)가 그 값으로 채워져서 그대로 찍혀 나온다. 아래 두 줄이 바로 그 "호출부"다:

<xacro:rod_link name="link1" mass="${m1}" length="${L1}"
                 radius="${rod_radius}" material_name="link1_blue"/>
<xacro:rod_link name="link2" mass="${m2}" length="${L2}"
                 radius="${rod_radius}" material_name="link2_orange"/>

이 두 줄이 xacro 처리를 거치면, 3-1에서 본 rod_link 매크로 정의를 기준으로 각각 완전한 <link name="link1">...</link>, <link name="link2">...</link> 블록으로 전개된다. 매크로가 없었다면 link1/link2 각각 <visual>/<collision>/<inertial>을 통째로 복붙해야 했을 것이다. 두 막대가 구조적으로 완전히 동일하다는 걸 코드 레벨에서도 드러낼 수 있다.

2-4. Gazebo / ros2_control 확장

URDF만 놓고 보면 "로봇의 생김새와 관절 구조"까지만 정의한다. 실제로 Gazebo에서 이 로봇을 움직이려면 어떤 물리엔진 플러그인을 붙일지, 각 관절을 토크로 제어할지 위치로 제어할지 같은 정보가 더 필요하다.
URDF 표준 자체에는 없지만, <gazebo> 태그로 시뮬레이터 전용 설정을 얹을 수 있다.

<gazebo>
  <plugin filename="gz_ros2_control-system"
          name="gz_ros2_control::GazeboSimROS2ControlPlugin">
    <parameters>$(find double_pendulum_control)/config/controllers.yaml</parameters>
  </plugin>
</gazebo>

ros2_control 태그로 이 로봇이 어떤 인터페이스로 명령을 받고 상태를
내보내는지 정의한다:

<ros2_control name="double_pendulum_system" type="system">
  <hardware>
    <plugin>gz_ros2_control/GazeboSimSystem</plugin>
  </hardware>
  <joint name="joint1">
    <command_interface name="effort">
      <param name="min">-60.0</param>
      <param name="max">60.0</param>
    </command_interface>
    <state_interface name="position"/>
    <state_interface name="velocity"/>
    <state_interface name="effort"/>
  </joint>
  <!-- joint2도 동일한 패턴 -->
</ros2_control>

이 태그들이 실제로 어떤 노드로 떠서 서로 연결되는지 rqt_graph로 찍어보면:

rqt_graph로 본 ros2_control 노드 관계

gz_ros2_control(URDF의 <plugin>이 로드하는 것) → controller_manager →effort_controller로 이어지고, effort_controller가/effort_controller/commands 토픽으로 명령을 받는걸 볼 수 있다.
위 xacro의 <command_interface name="effort">가 실제로는 이 토픽으로 연결된다는 뜻이다.

2-5. URDF 파일이 실제로 Gazebo에 뜨기까지

지금까지 본 double_pendulum.urdf.xacro는 로봇의 구조를 적어둔 파일일 뿐이다. 이 파일 하나로는 화면에 로봇을 띄울 수는 없고, 아래 4단계를 거쳐야 한다.

1단계. xacro → URDF 변환
.urdf.xacro 안에는 ${m1}, ${L1} 같은 변수와 <xacro:rod_link .../> 같은 매크로 호출이 그대로 남아있다. 완성된 XML로 만들어주려면 xacro 명령을 실행해줘야한다. 이 명령을 사용하면 변수/매크로가 전부 실제 숫자와 태그로 풀려서 완전한 순수 XML(표준 URDF)이 나온다:

xacro double_pendulum.urdf.xacro > double_pendulum.urdf

이렇게 뽑아낸 double_pendulum.urdf가 매크로/변수 없이 완성된 진짜 설계도다.

2단계. robot_state_publisher가 좌표 계산
robot_state_publisher는 ROS2에 기본으로 포함된 표준 패키지다.
1단계에서 나온 완성된 URDF를 읽어서 좌표 변환(TF)을 실시간으로 계산해서 계속 발행해주는 역할을 한다.(ex: base_link 기준으로 link1은 어디에 있고, link1 기준으로 link2는 어디에 있는지 등을 계산) RViz, Gazebo, 컨트롤러 코드 모두 이 TF 정보를 보고 각 link가 3D 공간 어디에 있는지를 알아낸다.

3단계. Gazebo spawn 노드가 실제로 로봇을 만들어 넣음
ros_gz_sim create 같은 노드가 robot_description 토픽(완성된 URDF가 포함되어있음)을 구독하고 있다가, 그 내용을 읽어서 진짜로 Gazebo 월드 안에 로봇 엔티티를 생성한다.

4단계. 이 순서를 묶어주는 launch 파일
위 1~3단계(+ 컨트롤러 매니저)를 정해진 순서대로 실행시켜주는 게 launch 파일이다. spawn.launch.py가 이 파이프라인 전체를 묶는 역할을 한다.

요약: URDF는 설계도. 그 설계도를 실제로 읽어서 Gazebo에 로봇을 띄우는 파이프라인(xacro 변환 → TF 발행 → spawn)은 double_pendulum_description 패키지 안에 구현해뒀다.

double_pendulum_description 패키지 실제 구조:

double_pendulum_description/
├── package.xml
├── CMakeLists.txt
├── urdf/
│   └── double_pendulum.urdf.xacro   ← 1단계 원본 (매크로/변수 포함)
├── launch/
│   ├── spawn.launch.py              ← 4단계: 1~3단계를 순서대로 실행
│   └── empty_world.sdf              ← Gazebo 월드 정의
└── meshes/                          ← 비어있음 (아래 참고)
  • meshes/가 비어있는 이유: 이중진자 시뮬레이터에는 정밀 3D 메시 없이 <geometry><cylinder> 같은 primitive 도형만으로도 충분했다.
  • spawn.launch.py가 xacro 커맨드로 .urdf.xacro를 런타임에 robot_description 파라미터로 전개해서 robot_state_publisher / Gazebo spawn 양쪽에 넘기는 구조 (URDF 파일을 직접 읽는 게 아니라, launch 시점에 Xacro → URDF XML 문자열로 변환)

0개의 댓글