AEGLOSLAB ROS 2 Jazzy 대전

제10부 시뮬레이션

10.2 Gazebo Harmonic과 ros_gz

10.2.1 이 장에서 배우는 것 / 선수 지식

Gazebo는 ROS 생태계의 기본 시뮬레이터다. 그런데 이 문장에는 함정이 있다 — "Gazebo"라는 이름이 두 개의 서로 다른 소프트웨어를 가리키기 때문이다(5.6.7).

구 Gazebo(Gazebo Classic)와 신 Gazebo는 아키텍처·플러그인·명령·ROS 연동이 전부 다르다. 인터넷 예제의 상당수가 Classic 기준이며 그대로 쓰면 동작하지 않는다. 이것이 Gazebo를 처음 쓰는 사람이 겪는 최대 혼란이다.

이 장은 Jazzy와 짝을 이루는 Gazebo Harmonic(신 Gazebo 계열)을 기준으로 한다(BK-05).

배우는 것:

  • 세대 구분 — 무엇이 다르고 어떻게 알아보는가.
  • 아키텍처 — gz-sim·플러그인·전송.
  • ros_gz 브리지 — 두 세계를 잇는 법.
  • 로봇 스폰 — URDF를 시뮬에 로드하기.
  • 월드 구성 — SDF와 시스템 플러그인.
  • launch 통합 — 실행 구성.
  • 성능 — RTF와 그 지배 요인.
  • 헤드리스 실행 — CI·서버.

선수 지식: 5.6(SDF·URDF·세대 구분), 5.3·5.4(URDF·xacro), 1.10.3(use_sim_time·/clock), 10.1(시뮬 전략).

정직: Gazebo의 버전·패키지 이름은 혼란스럽고 계속 변해 왔다. 아래는 Harmonic 계열 기준의 개념이며, 정확한 패키지명·명령·플러그인 이름은 Gazebo 문서와 ros_gz 저장소의 해당 버전이 정본이다(확인 필요).

실행 확인(2026-08-16): ros-jazzy-ros-gz-bridge·ros-gz-sim(둘 다 1.0.22)을 설치해 브리지 문법·YAML 키·방향 기호·시스템 플러그인 이름을 실물과 대조했고, 브리지를 실제로 실행해 이 장의 설정이 그대로 동작함을 확인했다(10.2.4.1). 다만 시뮬레이터 자체를 구동해 물리·렌더링을 확인한 것은 아니다 — 헤드리스 컨테이너에는 GPU·디스플레이가 없다. 이 장의 RTF·센서 렌더링 관련 서술은 여전히 미검증이다(목차 3).(→ 181차 확인: 렌더링이 필요 없는 센서(IMU)는 GPU 없이도 시뮬레이터 자체가 완전히 동작함을 확인. → 183차 확인: 렌더링이 필요한 카메라 센서도 GPU·Xvfb 없이 완전히 동작함을 확인했다 — 10.2.9의 실행 확인(183차) 참고. "GPU 없인 미검증"이라는 이 문장은 이제 시대에 뒤처졌다. → 184차 확인: 다만 렌더링 기반 센서 전체가 GPU 없이 "완전히" 동작하는 것은 아니다 — gpu_lidar는 시스템은 기동하지만 실제 거리값을 만들어내지 못했다(모든 값이 inf). 10.2.9의 실행 확인(184차)·10.3.2의 실행 확인(184차) 참고. "GPU 없이 렌더링 센서가 된다"는 카메라에 한정해서 읽어야 한다.)

10.2.2 세대 구분

5.6.7에서 예고한 것을 여기서 정리한다.

축Gazebo Classic신 Gazebo(Harmonic 등)
명령gazebo·gzservergz sim
라이브러리 접두어gazebo_*gz-*
ROS 연동gazebo_ros_pkgsros_gz
플러그인 형식libgazebo_ros_*.so시스템 플러그인
전송Gazebo transport(구)gz-transport
ros2_controlgazebo_ros2_controlgz_ros2_control(10.4)
센서 타입 예<sensor type="ray"><sensor type="gpu_lidar">
상태지원 종료 방향(확인 필요)현재 개발

예제를 볼 때 세대를 판별하는 법:

  • gazebo · gzserver · libgazebo_ros_ → Classic
  • gz sim · gz topic · ros_gz_sim · gz-sim → 신 Gazebo

함정: Classic 예제를 조금씩 고쳐 쓰려다 시간을 버리는 것이 흔하다. 태그 하나가 다른 것이 아니라 구조 전체가 다르므로, 신 Gazebo 문서와 ros_gz 예제를 출발점으로 삼아야 한다(5.6.7의 반복).

정직 → 실행 확인(2026-08-16). "Jazzy는 Harmonic과 짝"이라는 서술을 확인했다 — ros-jazzy-ros-gz-sim을 설치하면 gz-sim 8(= Harmonic)이 함께 들어온다(/opt/ros/jazzy/opt/gz_sim_vendor/lib/gz-sim-8/).

그리고 설치 방식이 중요하다. Jazzy의 ros_gz는 Gazebo를 ROS 벤더 패키지로 싣는다 — gz_sim_vendor·gz_common_vendor·gz_gui_vendor 등 16개 벤더 패키지가 /opt/ros/jazzy/opt/ 아래로 들어온다. 즉 OSRF 저장소를 따로 추가하지 않아도 apt install ros-jazzy-ros-gz-sim 하나로 Gazebo가 함께 설치된다. 다른 조합도 설치는 가능하나 그때는 호환성을 직접 확인해야 하며, 페어링의 정본은 Gazebo·ros_gz 문서다.

10.2.3 아키텍처

gz sim (서버) 물리 엔진 DART 등 센서 시스템 10.3 시스템 플러그인 모델·월드 동작 gz-transport 내부 토픽 ros_gz_bridge ROS 2 토픽 gz sim (GUI) 선택적
구성역할
서버(gz sim -s)물리·센서·플러그인 실행
GUI(gz sim -g)시각화·조작 — 분리 가능
시스템 플러그인모델·월드에 동작을 부여
gz-transportGazebo 내부 통신(ROS와 별개)
ros_gz_bridgegz 토픽 ↔ ROS 토픽

핵심: Gazebo는 ROS가 아니다. 자체 전송 계층을 쓰며, ROS와 통신하려면 브리지가 필요하다(10.2.4).

# 실행
gz sim -r worlds/my_world.sdf          # -r: 즉시 재생
gz sim -s -r worlds/my_world.sdf       # 서버만(헤드리스)
gz sim -g                              # GUI만(서버에 접속)

# 내부 토픽 확인 (ROS 명령이 아니다)
gz topic -l
gz topic -e -t /world/my_world/model/robot/joint_state

견해: gz topic -l과 ros2 topic list를 구분하는 것이 중요하다. 브리지되지 않은 gz 토픽은 ROS에서 안 보이고, 그 반대도 마찬가지다. "토픽이 안 보인다"의 원인이 브리지 누락인 경우가 매우 흔하다.

10.2.4 ros_gz 브리지

두 세계를 잇는 핵심 구성 요소다.

# CLI로 단일 브리지
ros2 run ros_gz_bridge parameter_bridge \
  /model/robot/cmd_vel@geometry_msgs/msg/[email protected]

브리지 표기 문법:

<토픽>@<ROS 타입>@<gz 타입>       # 양방향
<토픽>@<ROS 타입>[<gz 타입>       # gz → ROS
<토픽>@<ROS 타입>]<gz 타입>       # ROS → gz
기호방향
@양방향
[gz → ROS(센서 데이터)
]ROS → gz(명령)

설정 파일 방식(권장):

# config/bridge.yaml
- ros_topic_name: "/clock"
  gz_topic_name: "/clock"
  ros_type_name: "rosgraph_msgs/msg/Clock"
  gz_type_name: "gz.msgs.Clock"
  direction: GZ_TO_ROS

- ros_topic_name: "/scan"
  gz_topic_name: "/world/my_world/model/robot/link/laser_link/sensor/lidar/scan"
  ros_type_name: "sensor_msgs/msg/LaserScan"
  gz_type_name: "gz.msgs.LaserScan"
  direction: GZ_TO_ROS

- ros_topic_name: "/cmd_vel"
  gz_topic_name: "/model/robot/cmd_vel"
  ros_type_name: "geometry_msgs/msg/Twist"
  gz_type_name: "gz.msgs.Twist"
  direction: ROS_TO_GZ
Node(
    package='ros_gz_bridge', executable='parameter_bridge',
    parameters=[{'config_file': bridge_config_path}],
    output='screen',
)

10.2.4.1 실측 — 위 설정은 그대로 동작한다

실행 확인(2026-08-16). ros_gz_bridge 1.0.22에서 위 YAML을 그대로 적용해 브리지를 실행했다. 다섯 키(ros_topic_name·gz_topic_name·ros_type_name·gz_type_name·direction)와 값 GZ_TO_ROS·ROS_TO_GZ가 전부 수용됐고, 로그가 이렇게 출력됐다.

[ros_gz_bridge]: Creating GZ->ROS Bridge: [/clock (gz.msgs.Clock) -> /clock (rosgraph_msgs/msg/Clock)] (Lazy 0)
[ros_gz_bridge]: Creating ROS->GZ Bridge: [/cmd_vel (geometry_msgs/msg/Twist) -> /model/robot/cmd_vel (gz.msgs.Twist)] (Lazy 0)

명령행 방식의 방향 기호도 --help 원문과 일치한다 — @는 양방향, [는 Gazebo→ROS, ]는 ROS→Gazebo.

로그 끝의 (Lazy 0)이 이 책이 빠뜨린 것을 가리킨다. 브리지에는 지연(lazy) 생성 옵션이 있다.

- ros_topic_name: "/scan"
  gz_topic_name: "/scan"
  ros_type_name: "sensor_msgs/msg/LaserScan"
  gz_type_name: "gz.msgs.LaserScan"
  direction: GZ_TO_ROS
  lazy: true          # ← 구독자가 생길 때만 실제로 연결

lazy: true를 넣으면 로그가 (Lazy 1)로 바뀐다(실행 확인). 구독자가 없으면 브리지가 실제 연결을 만들지 않으므로 변환·전송 비용이 들지 않는다.

견해: 대용량 토픽에는 lazy: true를 기본으로 검토하라. 이것은 이 책이 1.4.2(lazy publisher)·7.2.4(lazy 구독)·7.8.3.4("필요한 것만 구독하라")에서 반복해 온 원칙이 브리지 계층에 구현된 것이다. 시뮬에서 카메라·포인트클라우드를 브리지로 넘기면 그 자체가 RTF를 낮추는데(10.2.8), 아무도 안 볼 때는 전혀 넘기지 않는 것이 맞다. 반대로 기록(11.3)이나 지연 측정처럼 "항상 흘러야" 하는 토픽에는 쓰지 말라 — 첫 구독자가 연결되는 순간의 연결 지연이 관측을 왜곡한다.

정직: 설정 파일 형식·필드 이름은 버전 의존이다 — 위는 1.0.22에서 실측한 것이며 다른 버전에서는 키가 다를 수 있다(확인 필요).

/clock 브리지가 필수다:

Gazebo 시뮬 시각 /clock (gz) 브리지 /clock (ROS) use_sim_time: true 인 노드들 1.10.3

함정: /clock을 브리지하지 않으면 use_sim_time: true인 노드가 시간 0에 멈춘다(1.10.3·1.10.9의 1번). 이것이 "노드가 아무것도 안 한다"의 흔한 원인이며, 시뮬 사용 시 가장 먼저 확인할 항목이다.

ros2 topic hz /clock            # 흐르는가
ros2 param get /some_node use_sim_time

함정: gz 토픽 이름이 길고 월드·모델 이름을 포함한다. 월드나 모델 이름을 바꾸면 브리지 설정이 깨진다. gz topic -l로 실제 이름을 확인해 설정하라.

이미지 브리지:

# 이미지는 전용 브리지가 효율적일 수 있다
ros2 run ros_gz_image image_bridge /camera/image

견해: 브리지가 성능 병목이 될 수 있다. 대용량 데이터(이미지·포인트클라우드)를 브리지하면 변환·복사 비용이 든다(7.8). 필요한 토픽만 브리지하고, 디버그용 토픽은 평소 끄는 것이 좋다.

10.2.5 로봇 스폰

URDF를 시뮬에 로드한다.

# robot_state_publisher가 발행하는 robot_description을 사용
spawn = Node(
    package='ros_gz_sim', executable='create',
    arguments=[
        '-topic', 'robot_description',
        '-name', 'my_robot',
        '-x', '0.0', '-y', '0.0', '-z', '0.1',
        '-Y', '0.0',
    ],
    output='screen',
)

또는 파일에서:

ros2 run ros_gz_sim create -file /path/to/model.sdf -name my_robot

URDF → SDF 변환이 일어난다(5.6.4). 여기서 고정 관절 링크 병합 문제가 발생할 수 있다.

함정: 5.6.4의 고정 관절 병합이 여기서 실제 문제가 된다. 센서를 단 링크가 병합되면 그 센서가 동작하지 않거나 프레임이 없다. preserveFixedJoint 계열 설정이 필요하다(확인 필요: 신 Gazebo에서의 방법).

함정: 스폰 z 좌표를 0으로 두면 로봇이 바닥에 파묻힌 상태로 시작해 튀어 오른다. 로봇 바닥에서 원점까지의 높이만큼 띄워야 한다. 조금 높게 두고 떨어뜨리는 것이 안전하다.

함정: robot_description 토픽에서 스폰하려면 robot_state_publisher가 먼저 기동돼야 한다(5.5). launch 순서를 보장해야 하며, OnProcessExit·타이머 지연 등을 쓴다(6.3.6).

10.2.6 월드와 시스템 플러그인

월드는 SDF 파일이다(5.6.2).

<?xml version="1.0"?>
<sdf version="1.10">
  <world name="my_world">

    <!-- 시스템 플러그인 — 월드 동작 -->
    <plugin filename="gz-sim-physics-system"
            name="gz::sim::systems::Physics"/>
    <plugin filename="gz-sim-user-commands-system"
            name="gz::sim::systems::UserCommands"/>
    <plugin filename="gz-sim-scene-broadcaster-system"
            name="gz::sim::systems::SceneBroadcaster"/>
    <plugin filename="gz-sim-sensors-system"
            name="gz::sim::systems::Sensors">
      <render_engine>ogre2</render_engine>
    </plugin>

    <physics name="1ms" type="ignored">
      <max_step_size>0.001</max_step_size>
      <real_time_factor>1.0</real_time_factor>
    </physics>

    <light type="directional" name="sun">
      <direction>-0.5 0.1 -0.9</direction>
      <diffuse>1 1 1 1</diffuse>
    </light>

    <model name="ground_plane">
      <static>true</static>
      <link name="link">
        <collision name="collision">
          <geometry><plane><normal>0 0 1</normal></plane></geometry>
          <surface>
            <friction><ode><mu>0.8</mu><mu2>0.8</mu2></ode></friction>
          </surface>
        </collision>
        <visual name="visual">
          <geometry><plane><normal>0 0 1</normal><size>100 100</size></plane></geometry>
        </visual>
      </link>
    </model>

  </world>
</sdf>

시스템 플러그인이 신 Gazebo의 핵심 개념이다. 물리·센서·장면 방송이 전부 플러그인이며, 필요한 것을 명시적으로 켜야 한다.

함정: 필요한 시스템 플러그인이 빠지면 그 기능이 조용히 없다. Sensors 플러그인이 없으면 센서가 동작하지 않고, SceneBroadcaster가 없으면 GUI에 아무것도 안 보인다. "센서 데이터가 안 나온다"의 원인이 이것인 경우가 있다.

모델 경로:

export GZ_SIM_RESOURCE_PATH=/path/to/models:$GZ_SIM_RESOURCE_PATH
# (확인 필요: 정확한 환경변수 이름)

견해: ament_environment_hooks로 이 경로를 자동 설정하라(4.1.5). 패키지를 소싱하면 모델 경로가 설정되게 하면, 개발자마다 수동으로 설정하는 부담이 사라진다.

로봇을 월드에 고정 포함하지 말라(5.6.6의 반복) — 실행 시 스폰하는 것이 재사용에 유리하다.

10.2.7 launch 통합

import os
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription, RegisterEventHandler
from launch.event_handlers import OnProcessExit
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue
from launch.substitutions import Command


def generate_launch_description():
    pkg = get_package_share_directory('my_robot_gazebo')
    desc_pkg = get_package_share_directory('my_robot_description')

    # ① Gazebo 실행
    gz_sim = IncludeLaunchDescription(
        PythonLaunchDescriptionSource(os.path.join(
            get_package_share_directory('ros_gz_sim'), 'launch', 'gz_sim.launch.py')),
        launch_arguments={'gz_args': f'-r {os.path.join(pkg, "worlds", "my_world.sdf")}'}.items(),
    )

    # ② robot_state_publisher (시뮬 인자로 xacro 처리 — 5.6.6)
    robot_desc = ParameterValue(
        Command(['xacro ', os.path.join(desc_pkg, 'urdf', 'robot.urdf.xacro'),
                 ' use_sim:=true']),
        value_type=str)
    rsp = Node(
        package='robot_state_publisher', executable='robot_state_publisher',
        parameters=[{'robot_description': robot_desc, 'use_sim_time': True}],
    )

    # ③ 스폰
    spawn = Node(
        package='ros_gz_sim', executable='create',
        arguments=['-topic', 'robot_description', '-name', 'my_robot', '-z', '0.15'],
    )

    # ④ 브리지
    bridge = Node(
        package='ros_gz_bridge', executable='parameter_bridge',
        parameters=[{'config_file': os.path.join(pkg, 'config', 'bridge.yaml'),
                     'use_sim_time': True}],
    )

    # ⑤ 컨트롤러 (10.4) — 스폰 후
    controllers = RegisterEventHandler(OnProcessExit(
        target_action=spawn,
        entities=[Node(package='controller_manager', executable='spawner',
                       arguments=['joint_state_broadcaster'])],
    ))

    return LaunchDescription([gz_sim, rsp, spawn, bridge, controllers])

순서가 중요하다(6.3.6):

순서이유
Gazebo → RSP → 스폰robot_description이 있어야 스폰
스폰 → 컨트롤러로봇이 있어야 컨트롤러 로드
브리지는 병렬 가능—

함정: use_sim_time을 모든 노드에 설정해야 한다(1.10.9의 2번). 하나만 빠져도 tf 외삽 오류가 대량 발생한다(5.2.4). /**: 파라미터 파일로 일괄 적용하는 것이 안전하다(1.9.7).

10.2.8 성능

RTF(Real Time Factor)가 핵심 지표다.

$$\text{RTF} = \frac{\text{시뮬 시간}}{\text{실제 시간}}$$

RTF의미
1.0실시간
< 1.0느림 — 시뮬 1초에 실제 여러 초
> 1.0실시간보다 빠름(단순 시뮬)
# GUI 하단에 표시되거나
gz topic -e -t /world/my_world/stats

RTF를 떨어뜨리는 요인:

요인영향
물리 스텝 크기작을수록 정확·느림
충돌 기하 복잡도메시가 비쌈(5.3.4)
센서 수·해상도렌더링 기반 센서가 가장 비쌈(10.3)
모델 수·복잡도월드 복잡도(10.1.8)
접촉 수물체가 많이 닿아 있으면
GUI 렌더링헤드리스로 개선

대처:

방법효과
헤드리스 실행GUI 렌더링 제거
센서 주기 낮추기큰 효과
센서 해상도 낮추기큰 효과
충돌 기하 단순화큰 효과(5.3.4)
물리 스텝 키우기정확도와 교환
월드 단순화10.1.8

견해: RTF가 낮으면 개발이 느려지고 CI가 오래 걸린다(10.1.7). 센서 설정이 가장 큰 지렛대인 경우가 많다 — 카메라 해상도를 절반으로 줄이면 픽셀이 4분의 1이 된다. 검증에 필요한 최소 해상도·주기를 쓰라(10.1.5의 최소 충실도 원칙).

함정: RTF < 1이면 use_sim_time을 쓰는 노드들은 정상이지만, wall timer를 쓰는 노드는 어긋난다(1.10.3·2.5.7). 시뮬에서 제어 주기가 비정상이라면 이것을 확인한다.

물리 스텝과 안정성:

스텝특성
1 ms정확·안정, 느림
2~5 ms일반적 절충
> 10 ms빠르지만 불안정·관통 가능

함정: 스텝이 크면 빠른 물체가 얇은 벽을 통과한다. 물리 시뮬의 고전적 문제이며, 로봇이 벽을 뚫고 나가는 현상으로 나타난다.

10.2.9 헤드리스 실행

CI·서버에서 GUI 없이 실행한다(10.1.7).

gz sim -s -r worlds/my_world.sdf          # 서버만
launch_arguments={'gz_args': '-s -r --headless-rendering worlds/my_world.sdf'}.items()
# (확인 필요: 렌더링 기반 센서를 헤드리스에서 쓰는 방법·옵션)(→ 183차 확인: `--headless-rendering` 플래그 없이도 자동으로 헤드리스 렌더링으로 전환됨을 확인 — 아래 실행 확인(183차) 참고)

정직: 헤드리스에서 카메라·GPU 라이다 같은 렌더링 기반 센서를 쓰려면 추가 설정이 필요할 수 있다(확인 필요)(→ 183차 확인: 카메라는 추가 설정이 필요 없었다 — Xvfb도 특정 렌더 백엔드 지정도 없이 자동으로 동작함을 실측. 아래 실행 확인(183차) 참고. → 184차 확인: gpu_lidar도 추가 설정 없이 기동은 됐지만("설정"의 문제는 아니었다), 실제 거리 데이터는 만들어내지 못했다(모든 값이 inf) — "설정 문제"가 아니라 별종의 미확인 한계다. 아래 실행 확인(184차) 참고) — 가상 디스플레이(Xvfb)나 특정 렌더 백엔드. CI 환경에서 GPU가 없으면 소프트웨어 렌더링이 되어 매우 느려진다.(→ 183차 확인: 간단한 장면(상자 하나, 320×240 카메라, 10Hz)에서는 RTF가 ~1.00으로 "매우 느려진다"는 서술과 맞지 않았다 — 장면 복잡도에 따라 달라질 수 있어 부분 확인으로 남긴다. 아래 실행 확인(183차) 참고)

실행 확인(183차) — GPU도 Xvfb도 없이 렌더링이 필요한 카메라 센서가 완전히 동작함을 실측했다. 10.2(2026-08-16 실행 확인)과 10.3(181차)이 남긴 "GPU 없이는 렌더링 센서 미검증"이라는 한계를 이 절에서 마저 닫는다. 지면 + 상자 + type="camera" 센서(320×240, 10Hz)가 있는 SDF 월드에 gz-sim-sensors-system(렌더링 담당, 181차가 IMU 시험에서 일부러 뺐던 바로 그 시스템)을 포함해서 gz sim -s -r(플래그 추가 없음, Xvfb 미설치)로 실행했다. 로그에 "[Wrn] Unable to open display: . Trying to run in headless mode."가 출력되더니 아무 문제 없이 계속 진행됐다 — Ogre2가 디스플레이 부재를 스스로 감지해 헤드리스(소프트웨어 렌더링, Mesa/EGL 경유)로 자동 전환한 것이다. /camera·/camera_info 토픽이 실재했고, /camera_info는 설정한 horizontal_fov·해상도로부터 정확히 계산된 카메라 내부 파라미터($f_x=f_y=277.19$, $c_x=160$, $c_y=120$ — 표준 핀홀 공식과 일치)를 실제로 발행했으며, /camera엔 step=960(=320×3, R8G8B8과 정확히 일치)의 실제 픽셀 데이터가 담긴 이미지 메시지가 흘렀다(메시지 크기 921,849바이트 — 빈 메시지가 아니라 실제 230,400바이트 원본 이미지에 해당). /world/.../stats로 측정한 RTF는 ~1.00으로, 이 간단한 장면(질감 없는 상자 하나)에서는 소프트웨어 렌더링이 "매우 느려진다"는 서술과 맞지 않았다 — 다만 장면 복잡도(질감·해상도·카메라 수)에 따라 이 결과가 어떻게 바뀌는지는 시험하지 않아, "느려지지 않는다"고 일반화하지는 않는다. 원본: tools/gap_harness/gap183/camera_test/(camera_world.sdf·camera_launch_full.log·camera_info_echo.log·rtf_stats.log).

실행 확인(184차) — 183차의 카메라 성공이 라이다에는 이어지지 않았다. 같은 헤드리스·소프트웨어 렌더링 조건에서 gpu_lidar 센서를 두 가지 독립된 배치(360포인트·벽 2m / 8포인트·대형 벽 0.5m, 지오메트리상 놓칠 수 없는 배치)로 시험했다. /scan 토픽은 정상 발행됐지만 두 배치 모두 전체 거리값이 inf였다(360/360, 8/8) — 벽 모델이 실제로 로드돼 있음(gz model --list로 확인)에도 레이가 아무것도 맞히지 못했다. 로그에는 관련 오류·경고가 전혀 없어 원인은 미상이다(추정: GPU 컴퓨트/셰이더 기반 레이 렌더링을 llvmpipe 소프트웨어 래스터라이저가 지원하지 못할 가능성 — 미확인). 별도로 구세대 CPU 표기인 type="lidar"도 시험했는데, 이번엔 GPU와 무관하게 [Wrn] [SdfEntityCreator.cc:1097] Sensor type LIDAR not supported yet. Try using a GPU LIDAR instead. 경고와 함께 센서 자체가 생성되지 않았다 — Gazebo Harmonic 8.11.0이 type="lidar"를 아직 구현하지 않은 것으로, 10.2.2 표가 신세대 표기 예로 gpu_lidar만 든 것과 정합적이다. 정직한 결론: "렌더링 기반 센서는 GPU 없이도 동작한다"는 183차의 카메라 확인에 한정한다 — 같은 조건에서 gpu_lidar는 시스템은 기동해도 실제 데이터를 만들지 못했다(원인 미상). 자세한 내용·원본은 10.3.2의 실행 확인(184차)과 tools/gap_harness/gap184/lidar_test/.

견해: CI에서 렌더링 센서가 필요하면 비용을 각오하라. 라이다·카메라 없이 검증할 수 있는 시나리오(로직·기구학 — 10.1.5의 ①②)를 CI에 두고, 센서가 필요한 것은 야간이나 GPU 러너에서 실행하는 분업이 실용적이다.

Docker에서:

# GPU 접근이 필요하면
# docker run --gpus all ...
# 없으면 소프트웨어 렌더링 — 느림

10.2.10 진단

# gz 쪽 확인
gz topic -l                          # 내부 토픽 목록
gz topic -e -t <토픽>                 # 내용
gz service -l                        # 서비스
gz model --list                      # 스폰된 모델
gz topic -e -t /world/<world>/stats  # RTF

# ROS 쪽 확인
ros2 topic list
ros2 topic hz /clock                 # ★ 가장 먼저
ros2 topic hz /scan
ros2 run tf2_tools view_frames       # tf(5.1.10)

증상별 원인:

증상원인
노드가 아무것도 안 함/clock 브리지 누락(10.2.4)
ROS에 토픽이 없음브리지 설정·gz 토픽 이름
센서 데이터 없음Sensors 플러그인·센서 정의(10.3)
로봇이 튀어 오름스폰 높이·관성(5.3.5)
로봇이 바닥을 뚫음물리 스텝·충돌 기하
센서 프레임 없음고정 관절 병합(5.6.4)
tf 외삽 오류use_sim_time 불일치(5.2.4)
RTF 낮음센서·물리·월드(10.2.8)
예제가 안 됨세대 불일치(10.2.2)

견해: ros2 topic hz /clock이 시뮬 진단의 첫 명령이다. 이것이 안 나오면 다른 모든 것이 무의미하다. Nav2의 tf 확인(8.1.6)과 같은 위치의 검증 단계다.

10.2.11 함정 카탈로그

#함정증상대처
1Classic 예제 사용동작 안 함세대 확인(10.2.2)
2/clock 브리지 누락노드가 멈춤브리지 설정
3use_sim_time 일부 누락tf 외삽 오류/**: 일괄(1.9.7)
4gz 토픽 이름 오류브리지 무동작gz topic -l 확인
5시스템 플러그인 누락기능이 조용히 없음월드에 명시
6스폰 z=0로봇이 튐높이 여유
7RSP 전에 스폰robot_description 없음launch 순서
8고정 관절 병합센서 프레임 없음보존 설정(5.6.4)
9물리 스텝 과대관통·불안정1~5 ms
10대용량 토픽 브리지성능 저하필요한 것만
11월드에 로봇 고정 포함재사용 불가실행 시 스폰
12모델 경로 수동 설정환경 불일치환경 훅(4.1.5)
13헤드리스에서 렌더 센서실패·매우 느림설정·분업
14RTF 무시개발·CI 느림센서·월드 조정

10.2.12 체크포인트

  • Gazebo 두 세대의 차이 여덟 축을 말할 수 있는가. 예제를 볼 때 세대를 판별하는 법은?
  • 신 Gazebo의 구성 요소 다섯 가지와, "Gazebo는 ROS가 아니다"의 실무적 함의는?
  • 브리지 표기의 세 기호와 각각의 방향을 말할 수 있는가.
  • /clock 브리지가 없으면 무슨 일이 생기는가. 시뮬 진단의 첫 명령은?
  • gz 토픽 이름이 깨지기 쉬운 이유와 확인 방법은?
  • 로봇 스폰의 세 가지 함정을 말할 수 있는가.
  • 시스템 플러그인이 신 Gazebo의 핵심 개념인 이유와, 누락 시 증상은?
  • launch 순서가 왜 중요하며, 어떤 순서여야 하는가.
  • RTF의 정의와 이를 떨어뜨리는 여섯 요인을 말할 수 있는가. 가장 큰 지렛대는?
  • 물리 스텝이 크면 생기는 문제는?
  • 헤드리스에서 렌더링 센서를 쓸 때의 고려사항과 실용적 분업은?
  • 시뮬 문제 진단표에서 "노드가 아무것도 안 함"의 원인은?

다음 장 10.3은 시뮬의 핵심 가치 중 하나 — 센서 시뮬레이션을 다룬다. 무엇이 사실적이고 무엇이 아닌가, 그리고 노이즈를 어떻게 다룰 것인가.