TT Lab
요금제

한국어

시작하기
블로그

블로그

ROS 2 와 Gazebo 로 로봇 주행 시뮬레이션: 바퀴가 말하는 「완벽」을 믿지 않기

상태: 초안 (2026-10-18), 실측. 측정은 2026-10-17 에 홈랩 쿠버네티스의 임시 파드 하나에서 했습니다. 노드는 미니 PC 한 대(Intel Core Ultra 계열 22스레드, 메모리 31 GiB)이고, 파드 상한은 CPU 3코어·메모리 4 GiB 이며 GPU 와 /dev/dri 는 주지 않았습니다(특권 없음, 추가 capability 없음). 이미지는 ros:jazzy(ros-base, Ubuntu 24.04.5)에 apt 로 ros-jazzy-ros-gz·ros-jazzy-turtlebot3-gazebo 를 더했습니다. 판은 Gazebo Sim 8.15.0(Harmonic), ros_gz 1.0.24, turtlebot3_gazebo 2.3.7 입니다. 반복은 B 2회, C 3회, 나머지 1회이며 표의 값은 실행마다 한 번 잰 값입니다(중앙값 아님). 원자료(rosbag2 mcap, 요약 JSON, 1초 간격 cgroup 표본)는 작업 디렉터리의 ROS 측정 폴더에 있고, 글에는 노드 이름과 주소를 가린 값만 옮겼습니다.

목차

1. 직관: 눈을 감고 걸음 수를 세는 로봇

눈을 감고 「열 걸음 앞으로, 왼쪽으로 90도」를 네 번 하면 처음 자리로 돌아온다고 믿기 쉽습니다. 걸음 수는 정확히 셌으니까요. 그런데 바닥이 미끄러우면 한 걸음이 덜 가고, 돌 때 발이 헛돌면 90도가 아니라 80도만 돕니다. 걸음 수만 세는 사람은 그 차이를 모릅니다.

바퀴 로봇의 오도메트리(odom) 가 바로 걸음 수 세기입니다. 바퀴가 몇 바퀴 돌았는지를 적분해 「지금 여기 있다」고 말합니다. 시뮬레이터는 여기에 한 가지를 더 줍니다. 물리 엔진이 아는 실제 위치(정답 자세) 입니다. 이 글은 두 값을 나란히 기록하면 무엇이 보이는지를 실측으로 보여 줍니다.

2. 구성: 파드 하나 안의 헤드리스 시뮬레이션

파드 하나 안에서 도는 헤드리스 시뮬레이션 쿠버네티스 임시 파드: CPU 상한 3코어, 메모리 상한 4 GiB, GPU 없음 gz sim -s (화면 없음)물리 1 ms, 라이다·IMU ros_gz_bridgegz 토픽 ↔ ROS 2 토픽 주행 노드 (50 Hz)/odom 보고 /cmd_vel 냄 rosbag2 기록기mcap 파일 한 개 /odom /cmd_vel 기록하는 토픽: /odom /cmd_vel /imu /gt_poses(정답 자세) /scan 정답 자세는 gz 의 dynamic_pose 를 브리지로 꺼내 odom 과 나란히 기록합니다. 실행 A: 원본 월드(렌더링 라이다) B: 물리만 C: B + 실시간 제한 해제
그림: 파드 하나 안에서 도는 헤드리스 시뮬레이션: gz sim, 브리지, 주행 노드, 기록기

이미지 비용도 쟀습니다. ros:jazzy 는 압축 283 MiB 이고 받는 데 18.3 s 가 걸렸습니다. 여기에 시뮬레이션 패키지를 apt 로 더하면 768개 패키지, 554 MB 를 받아 설치 뒤 2,637 MB 가 늘고, 272 s 가 걸렸습니다. 참고로 레지스트리 매니페스트로만 본 osrf/ros:jazzy-desktop 은 압축 1,203.9 MiB, jazzy-simulation 은 698.3 MiB 입니다(받지 않았습니다). 매번 설치하는 시간이 아깝다면 이 단계를 이미지로 굳히는 것이 다음 할 일입니다.

3. 네 가지 월드

이름 무엇이 다른가
A TurtleBot3 의 빈 월드 원본입니다. Sensors 시스템(ogre2)과 렌더링이 필요한 gpu_lidar 가 있어 --headless-rendering 으로 띄웠습니다.
A2 A 에 정적인 상자 두 개를 놓았습니다. 빈 월드에서는 라이다 값이 모두 무한대라 렌더링이 제대로 되는지 알 수 없기 때문입니다.
B Sensors 시스템을 뺐습니다. 바닥은 인라인 평면이고, 마찰은 원본 바닥과 같게 mu 100, mu2 50 입니다. 렌더링도 네트워크도 쓰지 않습니다.
C B 에서 real_time_factor 를 0 으로 바꿔 실시간에 묶지 않고 최대 속도로 돌립니다.

4. 주행 노드: 시간이 아니라 odom 을 보고 간다

주행 노드는 「5초 동안 직진」 같은 열린 루프가 아닙니다. /odom 을 보고 모서리 (1,0), (1,1), (0,1), (0,0) 을 차례로 찾아가는 닫힌 루프이고, 최대 0.2 m/s, 0.6 rad/s, 시뮬레이션 시각 기준 50 Hz 입니다. 실제 로봇의 웨이포인트 주행과 같은 방식이어야 「목표 대비 오차」가 제어기, 오도메트리, 물리의 합으로 읽히기 때문입니다. 핵심은 직진 단계의 몇 줄입니다.

if self.phase == 'drive':
    tx, ty = CORNERS[self.leg]
    dx, dy = tx - x, ty - y
    # 진행 방향 위로 남은 거리. 지나치면 음수가 된다.
    seg_heading = HEADINGS[self.leg - 1] if self.leg else 0.0
    along = dx * math.cos(seg_heading) + dy * math.sin(seg_heading)
    if along <= POS_TOL:                      # 5 mm 안이면 멈추고 돌기 시작
        self.send(0.0, 0.0)
        self.mark('corner')
        self.phase = 'turn'
        return
    bearing = math.atan2(dy, dx) if math.hypot(dx, dy) > 0.05 else seg_heading
    w = max(-W_MAX, min(W_MAX, 2.0 * wrap(bearing - th)))
    v = max(0.02, min(V_MAX, 0.8 * along))
    self.send(v, w)

(TT Lab 실험 코드 square_driver.py 에서 발췌했고 주석은 글에 맞게 줄이거나 더했습니다. 회전 목표는 모서리마다 절대 방위 0, π/2, π, −π/2 로 주어 회전 오차가 쌓이지 않게 했습니다.)

5. 결과: 실시간을 지키는 비용과 모서리 오차

실행 RTF 한 바퀴 벽시계 모서리 실제 오차 mm (1→4) 끝 위치 실제 오차 odom↔실제 최대 CPU 평균 anon 최대 gz RSS
A 렌더링 라이다 1.000 47.5 s 11, 33, 40, 25 16 mm 41 mm 0.91코어 467 MiB 515 MiB
A2 A + 상자 1.000 47.5 s 11, 47, 66, 43 50 mm 74 mm 0.92코어 466 MiB 509 MiB
B 물리만 1.000 47.6 s 17, 23, 14, 5 16 mm 26 mm 0.73코어 196 MiB 156 MiB
B 반복 1.000 47.5 s 15, 33, 28, 13 16 mm 32 mm 0.74코어 196 MiB 측정 안 함
C 최대 속도 11.11 4.3 s 19, 27, 20, 7 16 mm 39 mm 2.88코어 196 MiB 156 MiB
C 반복 2 11.76 4.1 s 20, 28, 20, 8 17 mm 39 mm 2.54코어 196 MiB 측정 안 함
C 반복 3 10.88 4.4 s 19, 28, 20, 7 16 mm 39 mm 2.86코어 196 MiB 측정 안 함

RTF(실시간 대비 속도)는 /odom 머리말의 시뮬레이션 시각이 늘어난 양을 기록기가 받은 벽시계 시각이 늘어난 양으로 나눈 값입니다. CPU 는 파드 cgroup 의 cpu.stat 을 1초마다 읽어 주행 구간을 평균했고, 메모리는 memory.stat 의 anon 최대값입니다. memory.current 는 3.0–3.6 GiB 로 나오지만 대부분 apt 설치 때 생긴 페이지 캐시여서, 실제로 쥔 메모리는 anon 으로 봅니다.

렌더링을 빼고 실시간 제한을 풀면 한 바퀴 벽시계 시간(s) A 원본(렌더링 라이다) 47.5 A2 A + 상자 두 개 47.5 B 물리만 47.6 C 물리만 + 최대 속도 4.3 CPU 평균(코어, 상한 3) A 원본(렌더링 라이다) 0.91 A2 A + 상자 두 개 0.92 B 물리만 0.73 C 물리만 + 최대 속도 2.88 anon 메모리 최대(MiB) A 원본(렌더링 라이다) 467 A2 A + 상자 두 개 466 B 물리만 196 C 물리만 + 최대 속도 196 C 의 RTF(실시간 대비 속도)는 11.11. 같은 51.7 s 시뮬레이션이 4.3 s 에 끝납니다.
그림: 렌더링 여부와 실시간 제한에 따른 벽시계 시간, CPU, 메모리

6. 바닥 마찰 한 줄을 빠뜨렸을 때

렌더링 없는 월드(B)를 처음 만들 때, 원본의 바닥 모델을 인라인 평면으로 바꾸면서 마찰 설정을 적지 않았습니다. 그러면 물리 엔진(ODE)의 기본값 mu 1 이 쓰입니다. 고친 뒤의 바닥은 다음 한 줄을 갖습니다.

FRIC = '<surface><friction><ode><mu>100</mu><mu2>50</mu2></ode></friction></surface>'  # 원본 바닥과 같게

아래 그림은 같은 주행 노드가 두 월드에서 돈 기록입니다. 그림을 누르면 rosbag 에 남은 실제 궤적을 4배속으로 다시 재생합니다.

바퀴로 센 위치(odom)와 실제 위치 같은 주행 노드, 같은 1 m 정사각형. 바닥 마찰 한 줄만 다릅니다. ▶ 누르면 재생(4배속) 바닥 마찰을 적은 월드 odom 이 본 끝 위치 오차: 5 mm 실제 끝 위치 오차: 16 mm, 방향 -1.0° 바닥 마찰을 빠뜨린 월드 odom 이 본 끝 위치 오차: 5 mm 실제 끝 위치 오차: 0.50 m, 방향 -38° 실제(시뮬레이터 정답) odom(바퀴 회전으로 적분) 목표 정사각형 두 위치 사이 거리
그림: 같은 주행 노드가 마찰이 다른 두 월드에서 남긴 odom 과 실제 궤적
실행 회전당 실제 각도 모서리 실제 오차 mm (1→4) 끝 위치 실제 오차 / 방향 odom 이 본 끝 위치 오차
B (마찰 있음) 89.2, 89.7, 89.7, 89.7° 17, 23, 14, 5 16 mm / −0.98° 5 mm
B 마찰 누락 80.3, 79.7, 80.3, 80.9° 10, 138, 344, 487 503 mm / −37.99° 5 mm
C 마찰 누락 78.0, 79.2, 80.5, 80.9° 19, 164, 399, 533 549 mm / −40.24° 4 mm

제자리 회전에서 바퀴가 미끄러져 90° 대신 약 80° 만 돌았고, 그 어긋남이 모서리마다 쌓였습니다. 그런데 odom 은 바퀴 관절 각도를 적분하므로 미끄러짐을 알 수 없습니다. 그래서 odom 은 끝까지 「5 mm 안에 돌아왔다」고 보고했습니다. 결과를 odom 만으로 판정했다면 이 월드는 시험을 통과했을 것입니다.

정답 자세가 맞는지도 따로 확인했습니다. 실행이 끝난 뒤 gz model -m burger -p 로 물은 위치를 /gt_poses 의 마지막 값과 비교했습니다. C 마찰 누락 실행에서 gz model 은 (−0.2505, 0.4883) m, 방향 −40.0° 를 돌려주었고, /gt_poses 는 (−0.2505, 0.4884) m, −40.2° 였습니다. 마찰이 있는 B 에서는 두 값이 (−0.0130, 0.0099) m 로 같았습니다.

교훈. 시뮬레이션에서 로봇의 성능을 판정할 때는 로봇이 스스로 추정한 위치와 시뮬레이터의 정답 위치를 함께 기록하고, 판정은 정답 쪽으로 합니다. 추정 위치만 보면 물리 설정의 실수가 로봇의 성공으로 보입니다. 반대로 정답만 보면 추정기가 얼마나 틀리는지 모릅니다. 둘의 차이(그림의 빨간 선)가 오도메트리 표류이고, 이것이 실제 로봇에서 위치 추정기를 따로 두는 이유입니다.

7. GPU 없이 도는 라이다

A 와 A2 는 렌더링으로 거리를 재는 gpu_lidar(360빔, 5 Hz)를 씁니다. 파드에는 GPU 도 /dev/dri 도 없었지만 --headless-rendering(EGL)으로 떠서 RTF 1.000 을 지켰습니다.

8. CI 로 쓸 수 있는 최대 속도 모드

C 는 real_time_factor 를 0 으로 둔 B 입니다. 1 m 정사각형 한 바퀴(시뮬레이션 47.7–51.7 s)가 벽시계 4.1–4.4 s 에 끝났고, RTF 는 10.88–11.76 이었습니다.

PR 마다 「로봇이 1 m 정사각형을 2 cm 안에서 돈다」를 4초짜리 시험으로 돌릴 수 있다는 뜻입니다. 단, 그 판정은 6절처럼 정답 자세로 해야 합니다.

9. 겪은 문제

10. 한계

11. 다음 편으로

다음 편은 탁구 로봇으로 넘어갑니다. 바퀴 대신 공 하나를 MuJoCo 에 올려, 반발과 회전이 시뮬레이터 설정 한 줄에 얼마나 흔들리는지 잽니다. 로봇 팔 자체의 기초(좌표계, 역기구학)는 함께 쓰이고 있는 「탁구 로봇 팔의 기초」 시리즈(목차)에서 다룹니다.

티티 한마디. 걸음 수만 세면 저는 늘 제자리에 돌아온 거예요. 그런데 바닥이 미끄러우면 실제로는 반 미터 옆에 서 있죠. 그래서 시뮬레이터에서는 꼭 「진짜 내 위치」도 같이 적어 둬요!

참고

모든 링크는 2026-10-18 에 열람해 열리는 것을 확인했습니다. 판은 측정에 쓴 판입니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다