Skip to main content
Summary

가상 실험실의 문을 열다

로봇을 처음 Gazebo 안에 띄우면 묘한 기분이 든다. 화면 속 바퀴가 굴러가고, 벽에 부딪히면 멈추고, LiDAR가 보이지 않는 광선을 쏘아 주변을 읽는다. 실제 책상 위에는 아무 로봇도 없는데 ROS2 토픽에는 /scan, /odom, /tf, /cmd_vel 같은 익숙한 이름들이 나타난다. 마치 로봇 한 대가 컴퓨터 안으로 이사 온 것 같다.

하지만 Gazebo는 단순한 3D 뷰어가 아니다. 로봇 모양을 예쁘게 보여 주는 도구에 그친다면 RViz와 크게 다르지 않을 것이다. Gazebo의 진짜 역할은 로봇이 살아갈 세계를 만들고, 그 세계 안에서 물리 법칙과 센서 데이터를 계산하는 것이다. 로봇이 앞으로 가라는 명령을 받으면 바퀴 조인트가 움직이고, 바닥과 바퀴 사이의 마찰이 계산되며, 몸체의 질량과 관성에 따라 흔들림이 생긴다. 벽이 있으면 LiDAR는 벽까지의 거리를 측정하고, 카메라는 조명과 물체를 반영한 이미지를 만들어 낸다.

ROS2를 배우는 입장에서 Gazebo는 네 가지 기둥으로 이해하면 좋다. 첫 번째는 로봇이 놓이는 환경인 월드다. 두 번째는 URDF나 SDF로 표현된 로봇 모델이다. 세 번째는 움직임과 충돌을 계산하는 물리 엔진이다. 네 번째는 ROS2와 이어지는 센서, 제어, 시간 인터페이스다. 이 네 가지가 맞물릴 때 우리는 실제 로봇을 켜기 전에 가상 환경에서 전체 시스템을 시험할 수 있다.

Gazebo에는 오래전부터 사용된 Gazebo Classic 계열과, Ignition Gazebo라는 이름을 거쳐 현재 Gazebo Sim으로 이어지는 계열이 있다. 새 ROS2 프로젝트에서는 gz sim 명령과 ros_gz 패키지를 사용하는 Gazebo Sim을 만나는 경우가 많지만, 기존 자료와 패키지에는 아직 Gazebo Classic 기반 예제도 많이 남아 있다. 중요한 것은 이름보다 구조다. 월드, 모델, 플러그인, 물리, 센서, ROS2 브리지를 이해하면 버전이 조금 달라도 길을 잃지 않는다.1

Gazebo 기초

월드 파일: 로봇이 살아갈 무대

Gazebo에서 월드는 로봇이 놓이는 무대다. 빈 바닥 하나만 있는 세계도 월드이고, 복도와 방, 책상, 의자, 창고 선반, 경사로, 조명이 모두 들어 있는 복잡한 실내 공간도 월드다. 자율주행 이동 로봇을 만들 때 월드는 단순한 배경이 아니다. 로봇의 센서가 무엇을 볼지, 경로 계획기가 어떤 장애물을 피해야 할지, SLAM이 어떤 구조를 지도로 만들지를 결정한다.

Gazebo Sim의 월드 파일은 보통 SDF 형식으로 작성한다. SDF는 Simulation Description Format의 약자로, 시뮬레이션에 필요한 세계와 모델을 표현하는 XML 기반 형식이다. URDF가 주로 로봇 하나의 링크와 조인트 구조를 설명하는 데 집중한다면, SDF는 월드 전체를 다룬다. 중력, 조명, 지면, 벽, 장애물, 센서, 플러그인, 물리 설정까지 포함할 수 있다.

아주 작은 월드 파일은 다음처럼 생겼다.

<?xml version='1.0'?>
<sdf version='1.9'>
  <world name='training_room'>
    <physics name='default_physics' type='ignored'>
      <max_step_size>0.001</max_step_size>
      <real_time_factor>1.0</real_time_factor>
    </physics>

    <plugin filename='gz-sim-physics-system'
            name='gz::sim::systems::Physics'/>
    <plugin filename='gz-sim-scene-broadcaster-system'
            name='gz::sim::systems::SceneBroadcaster'/>

    <light type='directional' name='sun'>
      <pose>0 0 10 0 0 0</pose>
      <diffuse>0.8 0.8 0.8 1</diffuse>
      <direction>-0.5 0.1 -0.9</direction>
    </light>

    <model name='ground_plane'>
      <static>true</static>
      <link name='link'>
        <collision name='collision'>
          <geometry>
            <plane>
              <normal>0 0 1</normal>
              <size>20 20</size>
            </plane>
          </geometry>
        </collision>
        <visual name='visual'>
          <geometry>
            <plane>
              <normal>0 0 1</normal>
              <size>20 20</size>
            </plane>
          </geometry>
        </visual>
      </link>
    </model>

    <include>
      <uri>model://box</uri>
      <pose>2 0 0.5 0 0 0</pose>
    </include>
  </world>
</sdf>

버전에 따라 플러그인 파일 이름이나 기본 월드 템플릿은 조금씩 달라질 수 있다. 그래도 구조는 크게 변하지 않는다. <world> 안에 물리 설정이 있고, 조명이 있고, 지면이나 장애물 같은 모델이 들어간다. <include>는 이미 만들어진 모델을 월드 안으로 불러오는 태그다. 창고 선반, 카페 테이블, 병원 복도, 주택 내부 같은 복잡한 모델을 매번 직접 쓰지 않고 재사용할 수 있다.

프로젝트에서는 월드 파일을 ROS2 패키지 안에 넣어 관리하는 편이 좋다. 예를 들어 다음처럼 my_robot_simulation 패키지를 만들 수 있다.

my_robot_simulation/
├── launch/
│   └── gazebo.launch.py
├── worlds/
│   ├── empty_room.sdf
│   ├── office.sdf
│   └── warehouse.sdf
├── models/
│   └── small_box/
└── package.xml

이렇게 해 두면 월드도 코드처럼 버전 관리할 수 있다. 오늘 실험한 환경이 마음에 들면 office.sdf로 저장하고, 내일 다시 같은 조건에서 실행한다. 시뮬레이션에서 재현성은 매우 중요하다. 로봇이 어제는 성공했고 오늘은 실패했다면, 로봇 알고리즘이 달라진 것인지 환경이 달라진 것인지 구분할 수 있어야 한다.

ROS2 launch 파일에서 특정 월드를 인자로 받도록 만들면 더 편하다.

import os

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution, TextSubstitution
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    world_arg = DeclareLaunchArgument(
        'world',
        default_value=PathJoinSubstitution([
            FindPackageShare('my_robot_simulation'),
            'worlds',
            'empty_room.sdf'
        ]),
        description='Gazebo world file'
    )

    pkg_ros_gz_sim = get_package_share_directory('ros_gz_sim')

    gazebo = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(
            os.path.join(pkg_ros_gz_sim, 'launch', 'gz_sim.launch.py')
        ),
        launch_arguments={
            'gz_args': [LaunchConfiguration('world'), TextSubstitution(text=' -r')],
            'on_exit_shutdown': 'true'
        }.items()
    )

    return LaunchDescription([
        world_arg,
        gazebo
    ])

이제 기본 월드를 실행할 수도 있고,

ros2 launch my_robot_simulation gazebo.launch.py

다른 월드를 지정할 수도 있다.

ros2 launch my_robot_simulation gazebo.launch.py world:=/home/user/robot_ws/src/my_robot_simulation/worlds/warehouse.sdf

Gazebo가 모델을 찾지 못한다면 리소스 경로 문제일 가능성이 높다. Gazebo Sim에서는 보통 GZ_SIM_RESOURCE_PATH를 사용한다. 이전 Ignition 계열 자료에서는 IGN_GAZEBO_RESOURCE_PATH, Gazebo Classic 자료에서는 GAZEBO_MODEL_PATH라는 이름을 볼 수 있다. 버전별 이름은 다르지만 의미는 같다. Gazebo에게 월드와 모델 파일이 어디 있는지 알려 주는 경로다.

로봇 모델: URDF가 Gazebo 안으로 들어갈 때

앞에서 우리는 URDF와 xacro로 로봇 모델을 만들었다. RViz에서는 링크와 조인트가 잘 보였고, robot_state_publisher가 /tf를 발행하면 로봇의 각 부품이 올바른 위치에 나타났다. 하지만 Gazebo는 한 가지를 더 묻는다. 이 로봇은 보기만 할 것인가, 아니면 물리 세계에서 움직일 것인가?

RViz에서 중요한 것은 시각화와 좌표계다. Gazebo에서 중요한 것은 시각화에 더해 충돌과 물리 계산이다. 그래서 Gazebo에서 쓸 로봇 모델에는 다음 요소가 제대로 들어 있어야 한다.

  • visual은 화면에 보이는 모양이다. 사람이 보기 좋게 메시 파일을 쓸 수 있다.

  • collision은 물체가 부딪히는 모양이다. 계산을 빠르고 안정적으로 하기 위해 단순한 박스, 원통, 구를 쓰는 경우가 많다.

  • inertial은 질량과 관성이다. 물리 엔진이 힘과 회전을 계산할 때 사용한다.

처음 모델을 만들 때 자주 하는 실수가 있다. 예쁜 3D 메시를 visual과 collision에 똑같이 넣는 것이다. 작은 로봇 하나라면 괜찮아 보일 수 있지만, 복잡한 메시끼리 계속 충돌 계산을 하면 시뮬레이션이 느려지고 불안정해진다. 겉모습은 자세히 만들되, 충돌 형상은 단순하게 만드는 편이 좋다.

예를 들어 로봇의 본체는 화면에는 둥근 모서리 메시로 보이게 하더라도, 충돌 계산에는 박스 하나를 사용할 수 있다.

<link name='base_link'>
  <visual>
    <geometry>
      <mesh filename='package://my_robot_description/meshes/base.dae'/>
    </geometry>
  </visual>

  <collision>
    <geometry>
      <box size='0.35 0.28 0.12'/>
    </geometry>
  </collision>

  <inertial>
    <mass value='4.0'/>
    <inertia ixx='0.04' ixy='0.0' ixz='0.0'
             iyy='0.05' iyz='0.0'
             izz='0.06'/>
  </inertial>
</link>

Gazebo 전용 설정이 필요할 때는 URDF 안에 <gazebo> 태그를 추가하거나, xacro에서 시뮬레이션 전용 매크로를 분리한다. 바퀴의 마찰, 센서 플러그인, 차동구동 플러그인, ros2_control 플러그인 같은 내용이 여기에 들어갈 수 있다. 또는 애초에 SDF 모델을 따로 유지하는 방법도 있다. 어느 방법이든 핵심은 하나다. 로봇의 링크와 조인트 구조, 충돌 형상, 관성값, 센서와 플러그인이 Gazebo가 이해할 수 있는 형태로 준비되어야 한다.

로봇을 월드에 넣는 과정을 흔히 스폰한다고 부른다. Gazebo Sim과 ros_gz_sim을 사용할 때는 create 실행 파일로 로봇을 삽입할 수 있다.

ros2 run ros_gz_sim create -topic robot_description -name my_robot -x 0.0 -y 0.0 -z 0.1

Gazebo Classic 기반 예제에서는 다음과 같은 spawn_entity.py 명령을 자주 볼 수 있다.

ros2 run gazebo_ros spawn_entity.py -topic robot_description -entity my_robot

둘은 사용하는 패키지와 Gazebo 계열이 다르지만 목적은 같다. ROS2 쪽의 robot_description에 담긴 로봇 모델을 Gazebo 월드 안에 등장시키는 것이다. 보통 launch 파일에서는 robot_state_publisher를 먼저 실행해 robot_description을 준비하고, 이어서 스폰 노드를 실행한다.

from launch import LaunchDescription
from launch_ros.actions import Node


def generate_launch_description():
    robot_state_publisher = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        parameters=[{
            'use_sim_time': True,
            'robot_description': '<robot>...</robot>'
        }]
    )

    spawn_robot = Node(
        package='ros_gz_sim',
        executable='create',
        arguments=[
            '-topic', 'robot_description',
            '-name', 'my_robot',
            '-x', '0.0',
            '-y', '0.0',
            '-z', '0.1'
        ],
        output='screen'
    )

    return LaunchDescription([
        robot_state_publisher,
        spawn_robot
    ])

여기서 -z 0.1처럼 로봇을 바닥보다 조금 위에 생성하는 이유는 충돌 초기 상태를 피하기 위해서다. 로봇이 바닥 안쪽에 박힌 채로 생성되면 물리 엔진이 강제로 밀어내는 과정에서 로봇이 튀어 오르거나 뒤집힐 수 있다. 화면 속 로봇이 갑자기 하늘로 날아간다면, 의외로 출발 높이와 충돌 형상부터 확인해야 한다.

물리 엔진: 보기 좋은 로봇과 잘 움직이는 로봇의 차이

Gazebo에서 물리 엔진은 보이지 않는 심판이다. 로봇이 바닥에 서 있을 수 있는지, 바퀴가 헛도는지, 벽에 부딪혔을 때 멈추는지, 조인트가 제한 각도를 넘지 않는지 계속 계산한다. 이 계산에는 질량, 관성, 마찰, 조인트 제한, 감쇠, 접촉 조건이 들어간다.

현실의 로봇은 조금 엉성해도 중력이 알아서 붙잡아 준다. 하지만 시뮬레이션에서는 숫자가 이상하면 세계가 이상해진다. 질량이 0에 가깝거나 관성값이 지나치게 작으면 로봇이 떨릴 수 있다. 바퀴 마찰이 너무 낮으면 로봇이 제자리에서 미끄러지고, 너무 높으면 회전이 부자연스러워질 수 있다. 충돌 형상이 실제보다 너무 크면 보이지 않는 벽에 부딪히고, 너무 작으면 장애물을 뚫고 지나가는 것처럼 보인다.

물리 설정을 잡을 때는 욕심을 줄이는 것이 좋다. 처음부터 완벽한 현실을 만들려고 하기보다 안정적으로 움직이는 단순한 모델을 만든 뒤 조금씩 정교하게 바꾼다. 이동 로봇의 경우 바퀴와 바닥의 접촉이 특히 중요하다. 바퀴 반지름, 바퀴 간 거리, 마찰 계수, 질량 중심이 조금만 이상해도 /cmd_vel 명령에 대한 움직임이 크게 달라진다.

Gazebo GUI에서 real time factor라는 값을 볼 수 있다. 이 값은 시뮬레이션 시간이 실제 시간에 비해 얼마나 빠르게 흐르는지를 나타낸다. 1.0에 가까우면 실제 시간과 비슷하게 계산되고 있다는 뜻이다. 값이 크게 떨어지면 컴퓨터가 물리와 센서 계산을 따라가지 못하고 있다는 뜻이다. 이때는 센서 업데이트 주기를 낮추거나, 충돌 형상을 단순화하거나, 너무 많은 모델을 줄이는 것이 도움이 된다.

/cmd_vel이 바퀴를 굴리는 길

이동 로봇을 움직이는 가장 익숙한 ROS2 명령은 /cmd_vel이다. 메시지 타입은 보통 geometry_msgs/msg/Twist이며, linear.x는 앞으로 가는 속도, angular.z는 제자리 회전 속도를 뜻한다. 실제 로봇에서는 이 명령을 받은 구동 노드가 모터 제어기로 속도 명령을 보낸다. Gazebo에서는 이 역할을 플러그인이나 ros2_control 구성이 대신한다.

가장 간단한 방식은 차동구동 플러그인을 사용하는 것이다. 차동구동은 왼쪽 바퀴와 오른쪽 바퀴의 속도 차이로 전진과 회전을 만드는 구조다. 플러그인에는 왼쪽 바퀴 조인트 이름, 오른쪽 바퀴 조인트 이름, 바퀴 반지름, 바퀴 간 거리, 명령 토픽 이름 등을 설정한다. 그러면 Gazebo 안의 플러그인이 /cmd_vel에 해당하는 속도 명령을 받아 바퀴 조인트를 움직인다.

좀 더 실제 로봇에 가까운 방식은 ros2_control을 사용하는 것이다. 이 경우 Gazebo 안에 가상의 하드웨어 인터페이스를 만들고, ROS2 쪽의 controller manager가 diff_drive_controller와 joint_state_broadcaster 같은 컨트롤러를 실행한다. 실제 로봇에서도 같은 컨트롤러 구조를 사용할 수 있기 때문에, 시뮬레이션과 실기 전환을 더 깔끔하게 만들 수 있다.

Gazebo Sim에서는 ROS2와 Gazebo Transport가 기본적으로 같은 통신망을 쓰지 않는다. Gazebo 내부 토픽과 ROS2 토픽 사이에 다리를 놓아야 한다. 이 다리 역할을 하는 대표 패키지가 ros_gz_bridge다.2

예를 들어 Gazebo 내부의 /model/my_robot/cmd_vel을 ROS2의 /cmd_vel로 연결하고 싶다면 다음과 같은 브리지를 실행할 수 있다.

ros2 run ros_gz_bridge parameter_bridge /model/my_robot/cmd_vel@geometry_msgs/msg/[email protected] --ros-args -r /model/my_robot/cmd_vel:=/cmd_vel

이제 ROS2에서 속도 명령을 발행한다.

ros2 topic pub /cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.2}, angular: {z: 0.5}}'

명령이 제대로 연결되어 있다면 Gazebo 안의 로봇이 앞으로 움직이면서 회전한다. 이때 로봇이 움직이지 않는다면 무작정 URDF를 의심하지 말고 흐름을 나누어 확인해야 한다. ROS2 쪽에서 /cmd_vel이 발행되는지, 브리지가 실행 중인지, Gazebo 쪽 명령 토픽 이름이 맞는지, 차동구동 플러그인의 조인트 이름이 실제 모델과 일치하는지 차례로 보면 된다.

로봇이 움직인 뒤에는 상태도 ROS2로 돌아와야 한다. 바퀴 조인트 상태가 /joint_states로 전달되고, robot_state_publisher가 이를 받아 /tf를 발행하면 RViz에서도 Gazebo와 같은 자세의 로봇을 볼 수 있다. Gazebo와 RViz가 같은 로봇 설명과 같은 조인트 상태를 공유하게 만드는 것이 핵심이다. Gazebo 공식 예제에서도 이 흐름은 Gazebo에서 계산된 조인트 상태를 ROS2로 전달하고, ROS2의 robot_state_publisher가 TF를 만드는 구조로 설명된다.3

가상 센서: 화면 속 센서도 토픽이 된다

Gazebo의 매력은 로봇이 움직이는 데서 끝나지 않는다. 실제 로봇에서 가장 비싸고 까다로운 센서들을 가상으로 붙여 볼 수 있다. 2D LiDAR는 주변 벽까지의 거리를 LaserScan 메시지로 만들고, 카메라는 이미지 메시지를 만들며, IMU는 가속도와 각속도를 발행한다. 이후 SLAM과 Nav2에서 사용할 데이터 흐름을 실제 하드웨어 없이도 연습할 수 있다.

센서는 보통 로봇 모델의 특정 링크에 붙는다. 예를 들어 LiDAR는 base_link 위의 laser_link에 달고, 카메라는 camera_link, IMU는 imu_link에 붙인다. 여기서 중요한 것은 센서 데이터의 frame_id와 TF가 맞아야 한다는 점이다. /scan 메시지의 frame_id가 laser_link인데 TF 트리에 laser_link가 없다면 RViz와 SLAM은 이 데이터를 어디에 놓아야 할지 모른다.

Gazebo Sim에서 센서 데이터가 ROS2로 나오는 흐름은 다음과 같이 생각하면 쉽다.

Gazebo 센서
  → Gazebo Transport 토픽
  → ros_gz_bridge
  → ROS2 토픽
  → RViz, SLAM, Nav2 노드

먼저 Gazebo 내부 토픽을 확인한다.

gz topic -l

그다음 ROS2 토픽을 확인한다.

ros2 topic list

처음에는 Gazebo 내부에는 센서 토픽이 있는데 ROS2에는 보이지 않을 수 있다. 이것은 고장이 아니라 브리지를 아직 놓지 않았다는 뜻일 수 있다. LiDAR를 ROS2의 /scan으로 받고 싶다면 다음과 같이 연결한다.

ros2 run ros_gz_bridge parameter_bridge /scan@sensor_msgs/msg/LaserScan[gz.msgs.LaserScan

카메라와 IMU도 같은 원리다.

ros2 run ros_gz_bridge parameter_bridge /camera/image@sensor_msgs/msg/Image[gz.msgs.Image
ros2 run ros_gz_bridge parameter_bridge /imu@sensor_msgs/msg/Imu[gz.msgs.IMU

실제 프로젝트에서는 이런 브리지를 매번 터미널에서 하나씩 실행하지 않고 YAML 파일이나 launch 파일로 묶는다. 하지만 처음 배울 때는 명령 하나로 연결하고, ros2 topic echo, ros2 topic hz, RViz를 이용해 데이터가 들어오는지 확인하는 편이 좋다.

LiDAR는 다음처럼 간단히 검사할 수 있다.

ros2 topic echo /scan --once
ros2 topic hz /scan

RViz에서는 Fixed Frame을 base_link, odom, 또는 현재 TF 트리에서 연결된 프레임으로 설정한 뒤 LaserScan 디스플레이를 추가한다. 점들이 로봇 주변에 둥글게 보이면 성공이다. ROS2 공식 Gazebo 튜토리얼에서도 Gazebo에서 생성한 LiDAR 데이터를 브리지로 ROS2에 전달하고 RViz에서 시각화하는 흐름을 기본 예제로 다룬다.4

센서가 보이지 않을 때는 세 가지를 함께 보아야 한다. 첫째, Gazebo 내부 토픽이 있는가. 둘째, ROS2 토픽으로 브리지되었는가. 셋째, 메시지의 frame_id가 TF 트리에 존재하는가. 세 번째가 빠지면 토픽은 분명히 들어오는데 RViz에는 아무것도 보이지 않는 일이 생긴다. 이때는 센서 자체보다 TF가 문제인 경우가 많다.

시뮬레이션 시간: 보이지 않는 첫 번째 설정

Gazebo를 ROS2와 함께 쓸 때 가장 많이 놓치는 설정이 use_sim_time이다. 실제 로봇에서는 시간이 컴퓨터의 벽시계 기준으로 흐른다. 하지만 시뮬레이션에서는 Gazebo가 만든 /clock이 기준이다. Gazebo를 일시 정지하면 시뮬레이션 시간도 멈추고, 시뮬레이션이 느리게 돌면 시간도 그에 맞추어 흐른다.

ROS2 노드가 시뮬레이션 시간을 사용하려면 파라미터를 켜야 한다.

Node(
    package='rviz2',
    executable='rviz2',
    parameters=[{'use_sim_time': True}]
)

또는 실행 중인 노드에 직접 설정할 수도 있다.

ros2 param set /rviz2 use_sim_time true

Gazebo Sim을 사용할 때는 /clock도 ROS2 쪽으로 브리지해야 할 수 있다.

ros2 run ros_gz_bridge parameter_bridge /clock@rosgraph_msgs/msg/Clock[gz.msgs.Clock

이 설정이 왜 그렇게 중요할까. 예를 들어 LiDAR 메시지는 시뮬레이션 시간 12.3초에 찍혔는데, RViz는 실제 컴퓨터 시간 18시 05분의 TF를 찾는다고 해 보자. 두 시간 기준이 다르면 RViz는 센서 데이터와 TF를 같은 순간의 정보로 맞출 수 없다. 그 결과 메시지 필터가 데이터를 버리거나, transform extrapolation 오류가 나타난다. SLAM과 Nav2에서는 문제가 더 커진다. 지도 작성, 위치 추정, 장애물 회피는 모두 센서 데이터와 로봇 위치의 시간 정렬에 의존하기 때문이다.

시뮬레이션에서 문제가 생겼을 때는 /clock부터 확인하는 습관을 들이자.

ros2 topic echo /clock --once
ros2 param get /rviz2 use_sim_time

/clock이 나오지 않으면 브리지나 Gazebo 실행 상태를 확인한다. /clock은 나오는데 시간이 증가하지 않으면 Gazebo가 일시 정지되어 있을 수 있다. Gazebo GUI의 재생 버튼을 눌러 시뮬레이션을 진행시켜야 한다. 로봇 소프트웨어가 멈춘 것이 아니라 세계의 시간이 멈춘 것이다.

Gazebo에서 문제가 생겼을 때 보는 순서

Gazebo 문제는 한 번에 모든 것을 의심하면 금방 지친다. 대신 데이터 흐름을 따라가면 된다. 로봇 시스템은 복잡해 보여도 결국 월드, 로봇, 명령, 상태, 센서, 시간의 연결이다.

  • 월드가 열리지 않으면 SDF 문법, 모델 리소스 경로, Gazebo 버전과 플러그인 이름을 확인한다.

  • 로봇이 보이지 않으면 robot_description, 스폰 명령, 초기 위치, 모델 경로를 확인한다.

  • 로봇이 튀거나 떨리면 질량, 관성, 충돌 형상, 바닥과의 초기 겹침, 조인트 제한을 확인한다.

  • /cmd_vel을 보내도 움직이지 않으면 ROS2 토픽, 브리지, Gazebo 내부 토픽, 차동구동 플러그인 또는 ros2_control 컨트롤러를 확인한다.

  • RViz에서 로봇이 다르게 보이면 /joint_states, /tf, robot_state_publisher, Fixed Frame을 확인한다.

  • 센서가 보이지 않으면 Gazebo 토픽, ROS2 브리지, 메시지 타입, frame_id, TF 연결을 확인한다.

  • SLAM이나 Nav2가 이상하면 /clock, use_sim_time, 센서 타임스탬프, TF 시간 오류를 먼저 확인한다.

이 목록은 디버깅 주문처럼 외우기 위한 것이 아니다. 로봇 소프트웨어를 흐름으로 보기 위한 지도다. Gazebo 안에서 로봇이 움직인다는 것은 명령이 들어가고, 물리가 계산되고, 상태가 나오고, 센서가 생성되고, 시간이 맞는다는 뜻이다. 어느 한 부분이 끊기면 현상은 비슷하게 보일 수 있지만 원인은 다르다.

작은 세계에서 자율주행으로

Gazebo를 이해하면 로봇 개발의 리듬이 달라진다. 실제 로봇을 켜고 배터리를 충전하고, 공간을 비우고, 충돌을 걱정하며 실험하던 일을 먼저 가상 환경에서 반복할 수 있다. 월드를 바꾸어 좁은 복도와 넓은 창고를 시험하고, 센서 노이즈를 조정하고, 장애물을 추가하고, 같은 실험을 여러 번 재현할 수 있다.

특히 SLAM과 Nav2를 다룰 때 Gazebo는 거의 필수적인 연습장이다. 단순한 fake node나 RViz 시각화만으로는 센서 데이터를 충분히 시험하기 어렵다. TurtleBot 계열 문서에서도 SLAM이나 Navigation처럼 센서가 필요한 실습에는 Gazebo가 적합하다고 설명한다.5 다음 단계에서 TurtleBot 시뮬레이션을 실행하면, 이 장에서 본 개념들이 한꺼번에 살아난다. 월드가 열리고, 로봇이 스폰되고, /cmd_vel이 바퀴를 굴리고, /scan이 RViz에 나타나며, /clock이 전체 시스템의 시간을 맞춘다.

그 순간부터 Gazebo는 단순한 프로그램이 아니다. 자율주행 로봇을 현실에 내보내기 전, 수없이 실패하고 다시 시도할 수 있는 안전한 실험실이 된다.

참고

1Tutorial 5: Simulation — Introduction to ROS

2Use ROS 2 to interact with Gazebo

3ROS 2 Interoperability — Gazebo documentation

4Setting up a robot simulation (Gazebo)

5TurtleBot3 Simulation

책 목록

ROS2로 완성하는 자율주행 로봇: 노드·TF·SLAM·Nav2 실전 설계

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기