AEGLOSLAB ROS 2 Jazzy 대전

제10부 시뮬레이션

10.3 센서 시뮬레이션과 플러그인

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

10.1.4의 충실도 ③단계 — 센서를 모델링하는 층이다. SLAM(8.2)·측위(8.3)·코스트맵(8.4)·인지(제7부)를 시뮬에서 검증하려면 여기가 필요하다.

그리고 여기가 Sim2Real 간극(10.6)의 가장 큰 원천이다. 시뮬 라이다는 완벽한 거리를 주고, 시뮬 카메라는 노이즈가 없고, 시뮬 IMU는 드리프트하지 않는다. 그 완벽함이 알고리즘을 실제보다 잘 동작하게 만들고, 실기에서 처음 문제를 만나게 한다.

이 장은 센서를 시뮬하는 방법과 함께 무엇이 사실적이지 않은가를 다룬다.

배우는 것:

  • 센서 종류와 플러그인 — 라이다·카메라·IMU·접촉·힘토크.
  • 노이즈 모델 — 무엇을 표현할 수 있는가.
  • 주기와 성능 — 센서가 RTF를 지배하는 이유.
  • 브리지 설정 — ROS로 내보내기.
  • 사실적이지 않은 것들 — 목록.
  • 실제에 가깝게 만들기 — 노이즈 주입 전략.
  • 검증 — 시뮬 센서가 규약을 지키는가.

선수 지식: 10.2(Gazebo·브리지), 7.1(센서 규약), 7.2~7.6(개별 센서), 5.6.4(고정 관절 병합), 10.1(전략).

정직: 센서 플러그인 이름·파라미터는 Gazebo 버전에 따라 다르다(확인 필요). 아래는 신 Gazebo 계열 기준의 개념이며, 정확한 SDF 요소는 Gazebo 문서가 정본이다. 노이즈 파라미터의 현실적 값은 센서마다 다르므로 측정이 필요하다. 이 환경에서 검증하지 않았다(목차 3).(→ 181차 확인: 상태 기반 센서(IMU)에 한해 실제로 gz-sim을 헤드리스로 구동해 검증했다 — 렌더링이 필요 없는 센서는 GPU·디스플레이 없이도 물리 시뮬레이터 자체를 실행할 수 있음을 확인, 10.2의 "시뮬레이터 구동은 GPU 없이 미검증"이라는 서술을 이 범위에서 갱신한다. 아래 실행 확인(181차) 참고. → 183차 확인: 렌더링이 필요한 카메라 센서도 GPU·Xvfb 없이 완전히 동작함을 확인했다(10.2.9) — "GPU 없인 렌더링 센서 미검증"이라는 제약 자체가 이제 낡은 서술이 됐다. → 184차 확인: 다만 이 "완전히 동작함"은 카메라에 한정된다 — 같은 조건에서 gpu_lidar는 토픽만 발행될 뿐 실제 거리값은 전부 inf였다(아래 10.3.2의 실행 확인(184차) 참고). "렌더링 기반 센서는 GPU 없이도 된다"를 라이다까지 일반화하지 않는다.)

10.3.2 센서 종류

센서SDF 타입ROS 메시지비용
2D 라이다gpu_lidar·lidar(→ 184차 확인: 이 버전(Gazebo Harmonic 8.11.0)에서 lidar는 실제로 생성되지 않는다 — 아래 실행 확인(184차) 참고)LaserScan(7.5)중간
3D 라이다gpu_lidar(다중 수직)PointCloud2(7.4)높음
카메라cameraImage+CameraInfo(7.2·7.3)높음
깊이 카메라depth_cameraImage(깊이)·PointCloud2높음
RGB-Drgbd_camera둘 다매우 높음
IMUimuImu(7.1.5)낮음
접촉contactContactsState계열(확인 필요)(→ 181차 확인·정정: Jazzy에서 실제 이름은 ros_gz_interfaces/msg/Contacts다 — 아래 실행 확인(181차) 참고)낮음
힘토크force_torqueWrenchStamped낮음
자력계magnetometerMagneticField낮음
GPSnavsatNavSatFix(7.6.6)낮음

비용 차이가 크다. 렌더링이 필요한 센서(라이다·카메라)가 지배적이고, IMU 같은 상태 기반 센서는 거의 무비용이다(10.2.8).

실행 확인(181차) — "IMU는 렌더링이 필요 없다"를 실측으로 확인했고, 부수적으로 10.2의 GPU 제약을 이 범위에서 갱신했다. Gazebo Harmonic 8.11.0을 최소 SDF 월드(지면 + 낙하하는 상자 + <imu> 센서)로 헤드리스 실행했는데, 시스템 플러그인에서 렌더링 담당(gz-sim-sensors-system)을 처음부터 빼고 물리(Physics)·SceneBroadcaster·Imu·UserCommands만 넣었다. gz sim -s -r(서버 전용, GUI 없음)로 실행하니 DART 물리 엔진이 정상 로드되고 GPU·렌더링 관련 오류 없이 완전히 기동했다 — gz topic -l로 /imu 토픽이 실제로 존재함을 확인했다. 이전 회차(90차)가 "시뮬레이터 구동은 GPU·디스플레이가 없어 미검증"이라 남긴 것은 카메라·라이다처럼 렌더링이 필요한 센서에 한정된 제약이었다는 것이 이번에 밝혀졌다 — 상태 기반 센서(IMU)만 쓰는 물리 시뮬레이션은 GPU 없이도 완전히 동작한다. 원본: tools/gap_harness/gap181/imu_sim_test/(imu_world.sdf·gz_sim_launch_run1.log·gz_topic_list.log).

실행 확인(184차) — "렌더링은 GPU 없이도 된다"(183차, 카메라)는 라이다에는 그대로 이어지지 않았다. Gazebo Harmonic 8.11.0을 헤드리스로 구동해 gpu_lidar 센서를 두 가지 독립된 월드로 시험했다: ① 360포인트 스캔, 벽까지 2m, ② 8포인트 스캔, 벽까지 0.5m(가로 10m·높이 3m 대형 벽 — 지오메트리상 놓칠 수 없는 배치, 라이다는 벽 바깥에 배치해 벽 속에 파묻히지 않게 함). 두 경우 모두 /scan 토픽은 정상 발행됐지만 모든 거리값이 inf(무응답)로 나왔다(360/360, 8/8) — gz model --list로 벽 모델이 실제로 로드돼 있음을 확인했음에도 레이가 아무것도 맞히지 못했다. 카메라(183차)는 소프트웨어 래스터라이저(llvmpipe)로 정상 동작했지만, gpu_lidar는 (추정, 미확인) GPU 컴퓨트/셰이더 기반 레이 렌더링을 쓰는 것으로 보이며 llvmpipe가 이를 지원하지 않는 것으로 짐작된다 — 근본 원인은 로그에서 관련 오류·경고를 찾지 못해 확인하지 못했다. 별도로 구세대 CPU 방식(10.2.2 표의 ray에 대응하는 표기인 type="lidar")도 시험했는데, 엔진이 [Wrn] [SdfEntityCreator.cc:1097] Sensor type LIDAR not supported yet. Try using a GPU LIDAR instead.라는 경고를 로그에 남기며 센서 자체를 만들지 않았다 — /scan 토픽이 전혀 생기지 않았다(첫 시도에서 백그라운드 실행 로그가 0바이트였던 것은 이 경고가 표준출력 버퍼링 시점 문제로 안 출력됐던 것으로 보이며, 포그라운드 재실행으로 경고를 확인했다). 이는 GPU·렌더링 문제가 아니라 Gazebo Harmonic 8.11.0 자체가 type="lidar"를 아직 구현하지 않은 것이다 — 위 표의 "gpu_lidar·lidar" 표기 중 lidar는 이 버전에서 실사용 불가능하며, 10.2.2가 신세대 표기 예로 gpu_lidar만 든 것과 정합적이다. 정직한 결론: 이 환경(헤드리스·소프트웨어 렌더링·GPU 없음)에서 gpu_lidar는 토픽·시스템은 정상 기동하지만 실제 거리 데이터를 만들어내지 못했다(원인 미상, 근본 원인은 추가 조사가 필요하다) — 183차의 "렌더링은 GPU 없이 된다"는 결론은 카메라에 한정하고, 라이다까지 일반화하지 않는다. 원본: tools/gap_harness/gap184/lidar_test/(lidar_world.sdf·scan_echo_raw.log·lidar_world_v2.sdf·scan_echo_v2.log·lidar_world_v3_cpu.sdf·lidar_launch_v3_verbose.log).

견해: IMU를 넣는 것은 비용이 낮으므로 대개 넣는 것이 좋다. 융합(7.6)을 시뮬에서 검증할 수 있고 RTF에 영향이 거의 없다. 반면 카메라를 여러 대 넣으면 RTF가 급락하므로, 정말 필요한지 확인하라(10.1.5).

10.3.3 센서 정의

URDF의 <gazebo> 확장 태그에 SDF 센서를 넣는다(5.3.7·5.4.9).

<!-- xacro 매크로 안, 시뮬 조건부 (5.6.6) -->
<xacro:if value="${use_sim}">
  <gazebo reference="${prefix}_link">
    <sensor name="${prefix}" type="gpu_lidar">
      <topic>${prefix}/scan</topic>
      <update_rate>10</update_rate>
      <always_on>true</always_on>
      <visualize>false</visualize>

      <lidar>
        <scan>
          <horizontal>
            <samples>720</samples>
            <resolution>1</resolution>
            <min_angle>-3.14159</min_angle>
            <max_angle>3.14159</max_angle>
          </horizontal>
          <vertical>
            <samples>1</samples>
          </vertical>
        </scan>
        <range>
          <min>0.15</min>
          <max>12.0</max>
          <resolution>0.01</resolution>
        </range>
        <noise>
          <type>gaussian</type>
          <mean>0.0</mean>
          <stddev>0.01</stddev>
        </noise>
      </lidar>
    </sensor>
  </gazebo>
</xacro:if>

reference가 링크 이름이며, 오타가 나면 조용히 무시된다(5.3.7의 함정).

카메라:

<gazebo reference="camera_link">
  <sensor name="camera" type="camera">
    <topic>camera/image</topic>
    <update_rate>30</update_rate>
    <camera>
      <horizontal_fov>1.047</horizontal_fov>
      <image>
        <width>640</width>
        <height>480</height>
        <format>R8G8B8</format>
      </image>
      <clip><near>0.1</near><far>100</far></clip>
      <noise>
        <type>gaussian</type>
        <mean>0.0</mean>
        <stddev>0.007</stddev>
      </noise>
      <optical_frame_id>camera_optical_frame</optical_frame_id>
    </camera>
  </sensor>
</gazebo>

함정: optical_frame_id를 설정하지 않으면 이미지의 frame_id가 광학 프레임이 아니게 된다(7.1.4). 그러면 3D 계산이 90도 회전한다. URDF에 광학 프레임을 정의하고 여기서 지정해야 한다(확인 필요: 요소 이름).

IMU:

<gazebo reference="imu_link">
  <sensor name="imu" type="imu">
    <topic>imu</topic>
    <update_rate>200</update_rate>
    <imu>
      <angular_velocity>
        <x><noise type="gaussian"><mean>0</mean><stddev>0.0003</stddev>
           <bias_mean>0.0000075</bias_mean><bias_stddev>0.0000008</bias_stddev>
        </noise></x>
        <!-- y, z 동일 -->
      </angular_velocity>
      <linear_acceleration>
        <x><noise type="gaussian"><mean>0</mean><stddev>0.017</stddev>
           <bias_mean>0.1</bias_mean><bias_stddev>0.001</bias_stddev>
        </noise></x>
        <!-- y, z 동일 -->
      </linear_acceleration>
    </imu>
  </sensor>
</gazebo>

견해: IMU의 바이어스 모델링이 중요하다. 실제 IMU의 주된 오차원은 백색 노이즈가 아니라 바이어스 드리프트이며(7.6.2), 그것 없이는 융합 알고리즘이 시뮬에서 지나치게 잘 동작한다. bias_mean·bias_stddev를 설정해 실제에 가깝게 만든다.

실행 확인(182차) — bias_mean이 실제로 노이즈와 구분되는 지속적 오프셋을 만든다는 것을 실측했다. 책의 노이즈 블록 패턴 그대로(mean·stddev·bias_mean·bias_stddev를 가속도·각속도 세 축 모두에) bias_mean을 가속도 0.3·각속도 0.05로, bias_stddev는 아주 작게(0.001·0.0000008) 설정한 월드와, bias_mean=0인 대조 월드를 각각 200개 표본으로 비교했다. 바이어스 있는 쪽에서 대조군 대비 평균 오프셋이 가속도 3축 모두 약 ±0.300, 각속도 3축 모두 약 ±0.050으로 설정값과 1% 이내로 정확히 일치했다 — 노이즈 표준편차(가속도 0.017·각속도 0.0003)로는 도저히 설명 안 되는 크기이므로, 이것이 화이트 노이즈가 아니라 실제 지속적 바이어스임을 확인한 것이다. 정직하게 덧붙인다 — 축마다 부호가 다르게 나왔다(X·Z축은 +, Y축은 −, 가속도·각속도 둘 다 동일한 패턴)(→ 183차 정정: 이 축별 부호 서술이 틀렸다 — 원시 로그(bias_stats_analysis.log)를 다시 대조하니 가속도는 X+·Y−·Z−, 각속도는 X+·Y−·Z+로 Z축에서만 서로 다르다(가속도·각속도가 "둘 다 동일한 패턴"이라는 것은 오독이었다). X·Y축은 두 센서가 일치하지만 Z축은 갈린다 — 이상 현상 자체(무작위로 설명 안 되는 재현 가능한 부호 반전)는 여전히 유효하며 원인 미상도 그대로다. 코디네이터가 재계산으로 발견해 정정을 요청했다). 세 축 모두 같은 양의 bias_mean을 설정했고 bias_stddev가 극히 작아 무작위로 부호가 뒤집힐 확률은 사실상 0인데도 이런 패턴이 나온 것은 Gazebo Harmonic의 IMU 노이즈 구현에 축별 부호 규약(또는 특이한 동작)이 있다는 뜻으로 보이나, 원인은 조사하지 않고 정직하게 미상으로 남긴다. 원본: tools/gap_harness/gap182/bias_test/(imu_bias_world.sdf·imu_nobias_world.sdf·gz_imu_biased_echo.log·gz_imu_nobias_echo.log·bias_stats_analysis.log).

10.3.4 노이즈 모델

시뮬이 표현할 수 있는 것과 없는 것을 구분해야 한다.

노이즈표현 가능비고
가우시안 백색 노이즈✅기본 지원
바이어스(IMU)✅bias_*
양자화부분적resolution
드롭아웃(측정 실패)⚠️ 제한적(확인 필요)실제로는 흔함
다중경로·반사❌유리·거울
재질별 반사율제한적
모션 블러❌카메라
롤링 셔터❌
운동 왜곡(라이다)⚠️(확인 필요)7.5.4
온도 드리프트❌
전기 노이즈·EMI❌

견해: "표현할 수 없는 것"이 Sim2Real 간극의 핵심이다(10.6). 라이다가 유리를 통과하고, 검은 물체를 못 보고, 반사면에서 비정상적인 값을 내는 것은 시뮬에 없다. 시뮬에서 완벽하게 동작하는 인지 알고리즘이 실기에서 실패하는 주된 이유다.

의도적 노이즈 주입 전략:

목적방법
알고리즘 강건성 검증노이즈를 실제보다 크게
실제와 유사하게실측 통계에 맞춤
최악 조건 시험극단적 노이즈
도메인 랜덤화매 실행마다 파라미터 변경(10.6.4)

견해: "실제보다 가혹한 시뮬"이 유용할 때가 있다. 노이즈를 실제의 2배로 주고도 알고리즘이 동작하면, 실기에서 여유가 있다는 뜻이다. 완벽한 시뮬보다 보수적인 시뮬이 안전한 판단을 준다.

함정: 노이즈 파라미터의 현실적 값을 모르면 무의미하다(10.1.8). 센서 사양서의 노이즈 밀도나 정지 상태 실측(7.1.7)에서 값을 얻어야 한다.

10.3.5 주기와 성능

센서가 RTF를 지배한다(10.2.8).

$$\text{렌더링 비용} \propto N_{\text{센서}} \times f_{\text{update}} \times N_{\text{광선·픽셀}}$$

설정영향
update_rate선형
해상도(samples·width×height)선형~제곱
센서 수선형
always_on켜면 구독자 없어도 갱신
<always_on>false</always_on>     <!-- 구독자 있을 때만 (확인 필요: 동작) -->
<visualize>false</visualize>      <!-- GUI 시각화 비활성 -->

실행 확인(182차) — 이 동작을 IMU로 실측 시도했으나, 정직하게 결론을 내지 못했다. always_on=true와 always_on=false 월드를 각각 구독자를 하나도 연결하지 않은 채 10초간 실행하고 /proc/<pid>/stat으로 서버 프로세스의 CPU 사용량을 측정했다 — true는 10초 동안 3.28 CPU초, false는 3.20 CPU초로 약 2.4% 차이만 나 유의미한 차이라고 보기 어려웠다(단발 측정, 반복 없음). IMU 자체가 "거의 무비용"인 센서라(10.3.2) 매 스텝 실행되는 물리 계산(정지한 상자를 시뮬하는 비용) 자체가 이 CPU 사용량을 지배했을 가능성이 커, 이 설계로는 센서 고유의 절약 효과를 분리해 낼 수 없었다. 10초 뒤 처음 구독을 연결했을 때 받은 첫 메시지의 시각도 두 경우 모두 경과 시간(≈10초)과 가깝게 나와 뚜렷한 차이를 보여 주지 못했다. 정직한 결론: 이 경량 상태 기반 센서에서는 always_on의 효과를 명확히 관측하지 못했다 — 렌더링이 필요한 센서(카메라·라이다)에서는 렌더링 패스 자체를 건너뛰는 효과가 커서 다르게 나타날 가능성이 있으나 이번엔 시도하지 않았다(확인 필요, 이월)(→ 183차 확인: 카메라 센서로 같은 시험을 재현했으나 다시 유의미한 차이를 못 찾았다 — 바로 아래 실행 확인(183차) 참고). 원본: tools/gap_harness/gap182/always_on_test/(cpu_compare.sh·alwayson_cpu_result.txt·alwaysoff_cpu_result.txt).

실행 확인(183차) — always_on을 렌더링 센서(카메라)로 재시험했으나, 이번에도 유의미한 신호를 찾지 못했다. 10.2.9에서 처음으로 GPU 없이 카메라 센서를 완전히 구동한 그 인프라를 그대로 써서, always_on=true·false 카메라 월드를 각각 구독자 없이 10초간 실행하고 CPU 사용량을 비교했다 — true 3.33 CPU초, false 3.27 CPU초로 약 1.8% 차이(182차의 IMU 결과 2.4%와 비슷한 수준으로 여전히 잡음 범위). Ogre2 렌더 컨텍스트 초기화("Loading plugin gz-rendering-ogre2")가 always_on·구독자 여부와 무관하게 시스템 로드 시점에 이미 일어난다는 것을 로그로 확인했다 — 매 프레임 렌더 호출은 always_on이 게이팅할 수 있어도, 이 간단한 장면(질감 없는 상자 하나, 320×240, 10Hz)·10초 측정 창으로는 그 차이를 물리 스텝·프로세스 기본 비용의 잡음 위로 끌어올리지 못했을 가능성이 크다. 정직한 결론: 두 종류(IMU·카메라)의 센서, 두 번의 독립 시도 모두 always_on의 CPU 절감 효과를 측정해 내지 못했다 — 더 복잡한 장면(고해상도·다중 카메라·질감)이나 더 긴 측정 창·반복 시행이 있어야 판가름 날 수 있는 질문으로 이월한다. 원본: tools/gap_harness/gap183/camera_test/(camera_alwayson_world.sdf·camera_alwaysoff_world.sdf·cam_alwayson_cpu_result.txt·cam_alwaysoff_cpu_result.txt).

견해: 검증에 필요한 최소 해상도·주기를 쓰라(10.1.5). SLAM 검증에 720점 10 Hz 라이다가 필요한가, 360점 5 Hz로 충분한가를 판단한다. 해상도를 절반으로 하면 비용도 절반이고, 대부분의 로직 검증에는 영향이 없다.

함정: 카메라 해상도가 RTF에 미치는 영향이 크다. 1920×1080은 640×480의 6.75배 픽셀이다. 시뮬에서는 낮은 해상도로 검증하고, 실기 해상도는 별도로 다루는 것이 실용적이다.

함정: always_on이 참이면 아무도 안 보는 센서도 계속 렌더링한다(7.2.4의 lazy 개념과 대조). 디버그용으로 넣어 둔 카메라가 RTF를 낮출 수 있다.

10.3.6 브리지 설정

센서 데이터를 ROS로 내보낸다(10.2.4).

# config/bridge.yaml
- ros_topic_name: "/scan"
  gz_topic_name: "/lidar/scan"
  ros_type_name: "sensor_msgs/msg/LaserScan"
  gz_type_name: "gz.msgs.LaserScan"
  direction: GZ_TO_ROS

- ros_topic_name: "/imu/data"
  gz_topic_name: "/imu"
  ros_type_name: "sensor_msgs/msg/Imu"
  gz_type_name: "gz.msgs.IMU"
  direction: GZ_TO_ROS

- ros_topic_name: "/camera/camera_info"
  gz_topic_name: "/camera/camera_info"
  ros_type_name: "sensor_msgs/msg/CameraInfo"
  gz_type_name: "gz.msgs.CameraInfo"
  direction: GZ_TO_ROS

이미지는 전용 브리지가 효율적일 수 있다:

Node(package='ros_gz_image', executable='image_bridge',
     arguments=['/camera/image'])

함정: CameraInfo를 함께 브리지하지 않으면 3D 계산이 불가능하다(7.3). 이미지만 브리지하고 CameraInfo를 잊는 것이 흔하다.

함정: gz 토픽 이름이 월드·모델 이름을 포함할 수 있다(10.2.4). gz topic -l로 실제 이름을 확인하라.

프레임 이름 문제:

함정: 시뮬 센서의 frame_id가 URDF의 링크 이름과 다를 수 있다. Gazebo가 자체 규칙으로 이름을 붙이거나 월드 접두어가 붙는 경우가 있다(확인 필요). tf에 없는 프레임이면 하류가 전부 실패하므로(5.2), 브리지 후 frame_id를 반드시 확인한다.

ros2 topic echo /scan --field header.frame_id --once
ros2 run tf2_ros tf2_echo base_link laser_link      # 그 프레임이 tf에 있는가

필요하면 리매핑·재발행 노드로 보정한다.

10.3.7 사실적이지 않은 것들

이 목록이 이 장에서 가장 중요하다.

실제시뮬
유리·거울을 라이다가 통과벽으로 인식
검은 물체가 안 보임정상 인식
반사면에서 비정상적인 값없음
먼지·비·안개대개 없음
햇빛에 카메라 포화없음
조명 변화·그림자제한적
운동 왜곡(7.5.4)없거나 제한적
센서 오염·긁힘없음
시야 가림(케이블·구조물)모델링한 것만
온도 드리프트없음
통신 지연·유실없음(3.9)
센서 간 간섭없음

견해: 이 목록이 "시뮬에서 되는데 실기에서 안 되는" 이유의 대부분이다. 특히 유리·검은 물체·햇빛은 실내외 로봇에서 매우 흔하고 시뮬에 전혀 없다.

대처:

방법설명
목록을 인지하고 실기 검증가장 현실적
인위적 모델링유리를 "라이다에 안 보이는 물체"로
노이즈 과다 주입강건성 확보(10.3.4)
실기 데이터 재생bag으로 실제 센서 데이터 사용(10.6.6)

견해: "실기 bag 재생"이 시뮬 센서의 한계를 우회하는 강력한 방법이다. 실제 라이다 데이터로 SLAM·측위를 시험하면 시뮬 센서의 완벽함 문제가 없다. 시뮬은 로직·물리에, bag은 센서 현실성에 쓰는 분업이 효과적이다(10.1.3의 경계와 일치).

10.3.8 규약 검증

시뮬 센서도 REP 규약을 지켜야 한다(7.1).

확인방법
frame_id센서 프레임인가·tf에 있는가(7.1.3)
스탬프시뮬 시각인가(1.10.3)
단위·부호REP 103(7.1.2)
광학 프레임카메라(7.1.4)
IMU 중력 포함정지 시 z≈9.81(7.1.5)
공분산0이 아닌 값(7.1.7)
LaserScan 시간 필드time_increment(7.5.3)
무효 값inf·NaN 규약(7.5.5)
# 7.1.9의 검증을 시뮬에서도
ros2 topic echo /imu/data --field linear_acceleration --once   # z ≈ 9.81
ros2 topic echo /scan --field header.frame_id --once
ros2 topic echo /scan --no-arr --once                          # 파라미터 확인

함정: 시뮬 IMU가 중력을 포함하지 않는 경우가 있다(확인 필요: 플러그인 설정)(→ 181차 확인: Jazzy·gz-sim 8의 기본 <imu> 설정은 중력을 포함한다 — 아래 실행 확인(181차) 참고). 실기와 다르면 융합 설정이 시뮬과 실기에서 달라져야 하는데, 이것은 단일 출처 원칙 위반이며 혼란의 원천이다.

실행 확인(181차) — 중력 포함 여부와 낙하 물리를 직접 측정했다. 상자가 지면에 착지해 정지한 뒤 gz topic -e -t /imu로 연속 3개 샘플을 확인하니 linear_acceleration.z = 9.8000(x·y는 사실상 0), angular_velocity도 0에 수렴 — 기본 <imu> 설정(별도 옵션 없음)이 중력을 포함한다는 것을 실측으로 확인했다(책의 "z≈9.81"과 0.1% 이내로 일치 — gz-sim의 기본 중력 상수 자체가 9.81이 아니라 9.8이다). 부수적으로 낙하 물리 자체도 정밀하게 확인했다 — 시뮬을 다시 시작해 t=0부터 캡처하니, 상자가 자유낙하하는 동안(t=0.10~0.42초)은 linear_acceleration.z = 0.0000(무중력 상태의 정확한 가속도계 물리 — 고유가속도=실제가속도−중력=(−g)−(−g)=0)이었고, 착지 시점(t≈0.44초)에 정확히 9.8000으로 전환됐다 — 0.9 m 낙하의 이론적 자유낙하 시간($\sqrt{2 \times 0.9 / 9.8} = 0.4286$초)과 시뮬레이션 스텝 오차 이내로 일치해, 물리 엔진이 실제로 동역학을 계산하고 있음을 독립적으로 재확인했다. 원본: tools/gap_harness/gap181/imu_sim_test/(gz_imu_echo.log·gz_imu_freefall_echo.log·imu_freefall_timeline.txt).

함정: 시뮬 센서의 공분산이 0인 경우가 있다(7.1.7). 그러면 융합이 시뮬 센서를 절대 신뢰해 실기와 다르게 동작한다. 브리지 후 공분산을 채우는 노드가 필요할 수 있다.

견해: 7.1.9의 "앞에 벽" 검증을 시뮬에서도 하라. 시뮬 센서가 규약을 지키는지 확인하지 않으면, 실기에서 규약 문제가 생겼을 때 "시뮬에서는 됐는데"가 되어 진단이 혼란스러워진다. 시뮬과 실기가 같은 규약을 지켜야 비교가 의미 있다(10.6.5).

10.3.9 센서 배치 검증

시뮬의 값싼 활용 중 하나다.

확인방법
사각지대로봇 주위에 물체를 두고 보이는지
자기 구조물 관측라이다가 로봇 몸체를 보는가(7.5.7)
시야 겹침여러 센서의 커버리지
높이 적절성낮은·높은 장애물 감지
설치 각도기울여야 하는가
# RViz2에서 시뮬 센서 데이터 관찰
# - 로봇 주위 360도에 물체를 배치
# - 어느 방향이 안 보이는지 확인

견해: 센서 배치는 하드웨어 제작 전에 시뮬로 검토할 수 있다(9.6.3의 작업 공간 검토와 같은 논리). 라이다를 10 cm 높이면 사각이 어떻게 변하는지 몇 분이면 확인된다. 제작 후 발견하면 재작업 비용이 크다.

견해: "자기 구조물 관측"을 시뮬에서 미리 확인하면 각도 필터 설정을 준비할 수 있다(7.5.7). 다만 시뮬은 모델링한 것만 보므로, 실물의 케이블·볼트 등은 실기에서 재확인해야 한다.

10.3.10 함정 카탈로그

#함정증상대처
1<gazebo reference> 링크명 오타센서가 조용히 없음이름 대조(5.3.7)
2고정 관절 병합으로 링크 소멸센서 프레임 없음보존 설정(5.6.4)
3광학 프레임 미설정3D가 90도 회전optical_frame_id(7.1.4)
4CameraInfo 미브리지3D 계산 불가함께 브리지
5frame_id가 tf에 없음하류 전부 실패확인·보정
6IMU 바이어스 미모델링융합이 지나치게 잘 됨bias_* 설정
7시뮬 IMU 중력 미포함실기와 설정 상이규약 확인(7.1.5)
8공분산 0융합이 절대 신뢰값 채우기(7.1.7)
9해상도·주기 과도RTF 급락최소 충실도
10always_on 방치안 쓰는 센서가 비용필요 시만
11시뮬 센서의 완벽함 과신실기에서 실패한계 목록 인지(10.3.7)
12노이즈 값을 임의로무의미한 모델사양·실측 기반
13규약 검증 생략시뮬·실기 비교 무의미7.1.9 수행
14센서 배치를 제작 후 검토재작업시뮬에서 사전 검토

10.3.11 체크포인트

  • 센서 종류별 시뮬 비용 차이를 말할 수 있는가. IMU를 넣는 것이 값싼 이유는?
  • 센서 정의가 URDF 어디에 들어가며, 링크 이름 오타의 증상은?
  • optical_frame_id를 설정하지 않으면 무슨 일이 생기는가.
  • IMU 바이어스 모델링이 중요한 이유를 7.6.2와 연결해 설명할 수 있는가.
  • 노이즈로 표현 가능한 것과 불가능한 것을 각각 네 가지 들 수 있는가.
  • "실제보다 가혹한 시뮬"이 유용한 이유는?
  • 렌더링 비용의 식과 이를 줄이는 방법 네 가지를 말할 수 있는가.
  • always_on이 참일 때의 낭비와, 7.2.4의 어떤 개념과 대조되는가.
  • 브리지에서 흔히 빠뜨리는 것과, frame_id 문제를 확인하는 명령은?
  • "사실적이지 않은 것" 목록에서 실무적으로 가장 흔한 셋은 무엇인가.
  • 시뮬 센서의 한계를 우회하는 강력한 방법과, 시뮬과 bag의 분업은?
  • 시뮬 센서가 REP 규약을 지키는지 확인해야 하는 이유는?
  • 센서 배치를 시뮬로 미리 검토하는 이점과, 실기에서 재확인해야 하는 것은?

다음 장 10.4는 시뮬 로봇을 실제로 제어하는 층 — gz_ros2_control을 다룬다. 같은 URDF·같은 컨트롤러로 시뮬과 실기를 실행하는 구조.