로봇 팔 제어 코드에 지능은 없었습니다

파란 정육면체를 집어 옆으로 옮기는 예제입니다. Isaac Sim에서 제가 처음 돌려 본 것이고, 보고 있으면 로봇이 알아서 하는 것처럼 보였습니다. 코드를 열어 봤더니 좌표 일곱 개를 순서대로 읽어 주는 일이더군요. 그리고 실물 로봇에는 있을 수 없는 줄이 하나 섞여 있었습니다.

같은 이름의 파일이 두 개였습니다

제가 실행한 파일을 먼저 열었습니다. 그런데 정작 로봇을 움직이는 내용이 없었어요. 하는 일이 셋뿐이더군요. 시뮬레이터를 켜고, 작업 객체를 하나 만들고, 반복문 안에서 그 객체에게 "한 단계 진행해"라고 부르는 것.

실제 알맹이는 다른 폴더에 이름이 똑같은 파일로 들어 있었습니다. 실행용 예제 폴더에 하나, 확장 기능 폴더에 하나. 같은 파일명이 두 곳에 있으니 처음에는 잘못 찾은 줄 알았습니다.

C:\isaacsim\standalone_examples\api\
    isaacsim.robot.experimental.manipulators\franka\pick_place.py   ← 실행용 껍데기 · 112줄

C:\isaacsim\exts\isaacsim.robot.experimental.manipulators.examples\
    isaacsim\robot\experimental\manipulators\examples\franka\pick_place.py   ← 내용물 · 326줄

▲ 두 파일의 실제 경로를 제 PC에서 그대로 옮겨 적은 것입니다. 이름은 같고 길이는 세 배 차이가 납니다. 아래쪽 경로에서 점으로 이어진 확장 이름이 폴더가 되고, 그 안에서 같은 단어들이 슬래시로 다시 반복되는 것이 보이실 겁니다.

이 구조를 이해하는 데 시간이 좀 걸렸는데, 알고 보면 단순했습니다. 실행용 파일은 시동을 거는 껍데기이고, 확장 기능 쪽이 내용물입니다. 예제를 돌려 보는 것만으로는 아무것도 배울 수 없는 이유가 여기 있었습니다. 제가 읽은 파일에 정작 배울 것이 없었던 것이죠.

확장 기능 폴더의 경로가 유난히 깊고 길었습니다. 점으로 이어진 확장 이름이 폴더 하나로 쓰이고, 그 안에서 같은 이름이 슬래시로 나뉘어 또 반복되더군요. 익숙해지기 전까지는 탐색기에서 길을 잃기 딱 좋습니다.

굳이 왜 이렇게 나눴을까 싶었는데, 껍데기 쪽을 다시 보니 이해가 갔습니다. 같은 내용물을 화면에서 버튼으로 눌러 쓸 수도 있고 스크립트로 돌릴 수도 있게 해 둔 것이죠. 내용물을 한 벌만 두고 입구를 둘로 낸 셈입니다.

껍데기 파일에서 뜻밖의 것도 하나 발견했습니다. 실행할 때 역기구학 계산 방식을 네 가지 중에 골라 넣을 수 있게 되어 있더군요. 코드를 고치지 않고 옵션만 바꿔서요. 왜 한 가지로 정해 두지 않고 네 개를 남겨 놨는지, 그게 다음 글의 주제가 됐습니다.

일곱 단계를 표로 옮겨 봤습니다

내용물 파일을 열어 보니 일곱 단계짜리 상태 기계였습니다. 단계마다 몇 번씩 반복할지가 숫자로 미리 박혀 있었고요. 그 숫자를 그대로 옮겨 그려 봤습니다.

self.events_dt = [
    60,  # 단계 0: 큐브 위 x,y 위치로 이동
    40,  # 단계 1: 큐브 쪽으로 하강
    20,  # 단계 2: 그리퍼를 닫아 파지
    40,  # 단계 3: 큐브 들어 올리기
    80,  # 단계 4: 목표 위치로 이동
    20,  # 단계 5: 그리퍼를 열어 놓기
    20,  # 단계 6: 위로 물러나기
]

▲ 내용물 파일에 그대로 적혀 있는 부분입니다. 주석은 원문이 영어라 제가 옮겼습니다. 대단한 계획처럼 보이지만 실제로는 숫자 일곱 개가 전부입니다.

블록 하나 옮기는 데 일곱 단계, 280번의 계산막대 길이가 각 단계에 배정된 계산 횟수입니다Humanoid NowHumanoid NowHumanoid NowHumanoid NowHumanoid NowHumanoid NowHumanoid Now0601402203404805206200 · 큐브 위쪽 20cm로 이동1 · 큐브 가까이 하강2 · 그리퍼 닫기3 · 들어 올리기4 · 목표 위치로 이동 (가장 긺)5 · 그리퍼 열기6 · 물러나기주황 = 그리퍼 개폐, 파랑 = 팔 이동Humanoid Now합계 280 단계 · 물리가 초당 60회 돌아간다고 놓으면 약 4.7초짜리 동작단계마다 도착 여부를 확인하지 않고, 정해진 횟수를 채우면 다음으로 넘어갑니다자료: Isaac Sim 6.0 예제 코드 직접 확인 · Humanoid Now · humanoidnow.kr

▲ 위 숫자 일곱 개를 막대 길이로 바꿔 직접 그린 그림입니다. 단계 4가 유독 긴 이유는 팔이 가장 멀리 이동하는 구간이기 때문이고, 주황색 두 칸은 좌표 계산이 아예 없는 그리퍼 개폐 구간입니다.

숫자를 다 더해 보니 280이었습니다. 물리 계산이 초당 60번 돈다고 놓고 어림하면 5초가 안 되는 동작이죠. 다만 이건 기본값을 가정한 계산이라 실제 화면에서 느껴지는 시간과는 차이가 있을 수 있습니다.

표로 옮기고 나서야 눈에 들어온 게 있습니다. 단계를 넘어가는 조건이 "도착했는가"가 아니라 "정해진 횟수를 채웠는가"였습니다. 로봇이 목표에 닿았는지는 아무도 확인하지 않죠. 60번 계산했으면 그냥 다음으로 갑니다.

그러니까 이 숫자들은 넉넉하게 잡아 둔 여유 시간입니다. 60번이면 도착하고도 남는다는 경험값인 셈이죠. 반대로 말하면 이 숫자를 줄이면 도착하기 전에 다음 단계로 넘어갑니다. 아직 허공에 있는데 손가락을 닫는 장면을 보게 될 겁니다. 조만간 직접 줄여 보고 그 장면을 찍어 둘 생각입니다.

숫자만 봐서는 감이 잘 오지 않아, 예제를 다시 돌리면서 터미널을 같이 띄워 놓고 봤습니다. 단계가 바뀔 때마다 한 줄씩 올라오더군요.

Starting pick-and-place execution
Phase 0: Moving to x,y position above cube...
Phase 1: Approaching cube...
Phase 2: Closing gripper...
Phase 3: Lifting cube...
Phase 4: Moving cube...
Phase 5: Opening gripper...
Phase 6: Moving up...
Done picking and placing

▲ 예제를 돌릴 때 터미널에 찍히는 내용입니다. 로봇이 무엇을 판단했다는 말은 한 줄도 없고, 몇 번째 단계로 넘어갔다는 통보만 있습니다.

화면과 로그를 나란히 놓고 보니 단계 4에서 유독 오래 멈춰 있었습니다. 80번이 배정된 구간이죠. 팔이 큐브를 들고 반대편으로 건너가는 동안, 로그는 아무 말도 하지 않고 기다립니다. 저는 이걸 보고서야 그 숫자 80이 무슨 뜻인지 실감했습니다.

단계마다 하는 계산은 두 줄뿐이었습니다

단계 안쪽을 들여다보면 더 단출합니다. 큐브가 지금 어디 있는지 물어보고, 거기에 높이를 얼마쯤 더해 목표 좌표를 만들고, 그 좌표로 가라고 적어 넣는 것. 실질적으로 두세 줄입니다.

# 단계 0 — 큐브 위쪽으로 이동
cube_pos = self.cube.get_world_poses()[0].numpy()
goal_position = np.array([cube_pos[0, 0], cube_pos[0, 1], cube_pos[0, 2] + 0.2])
self.robot.set_end_effector_pose(position=goal_position, ...)

▲ 일곱 단계 가운데 한 단계의 전부입니다. 위치를 묻고, 20cm를 더하고, 가라고 적는다. 나머지 단계도 더하는 숫자만 바뀔 뿐 구조가 같았습니다.

단계 0은 큐브 위 20cm, 단계 1은 10cm. 그러니까 바로 내려가지 않고 일부러 위에서 한 번 멈춥니다. 처음엔 왜 두 번에 나눠 가나 싶었는데, 비스듬히 곧장 접근하면 손가락이 큐브를 옆으로 쳐 버립니다. 위에서 수직으로 내려와야 하고요. 사람도 물건 집을 때 무의식적으로 그렇게 합니다.

그리퍼를 여닫는 단계는 좌표 계산조차 없습니다. 손가락 관절 두 개에 값을 쓰는 게 전부죠. 손가락도 그냥 관절일 뿐이라는 걸 여기서 알았습니다. 특별한 장치가 아니었습니다.

큐브 치수도 코드에 박혀 있었습니다. 한 변이 5.15cm. 딱 떨어지는 5cm도 아니고 어중간한 숫자라 한참 들여다봤습니다. 누가 대충 정한 값은 아닐 텐데 왜 하필 이 숫자일까. 답은 로봇 쪽 파일에 있었고, 그건 다음 글에서 풀겠습니다. 힌트만 적자면 이 로봇이 벌릴 수 있는 손가락 폭과 관계가 있었습니다.

결국 "집어서 옮기기"라는 작업에 지능이라고 부를 만한 것이 하나도 없었습니다. 순서와 좌표, 그리고 기다리는 횟수. 그게 전부였습니다. 로봇이 똑똑해 보였던 건 제가 결과만 봤기 때문이었죠.

실물 로봇에는 있을 수 없는 한 줄

그런데 단계마다 반복되는 줄 하나가 계속 걸렸습니다. 큐브가 지금 어디 있는지 시뮬레이터에게 물어보는 줄입니다. 시뮬레이션이니까 당연히 답을 알고 있죠. 큐브를 만든 게 그 프로그램 자신이니까요.

cube_pos = self.cube.get_world_poses()[0].numpy()

▲ 이 한 줄입니다. 단계 0, 1, 6에 똑같이 들어 있습니다. 세어 보면 326줄짜리 파일에서 가장 여러 번 반복되는 줄이고, 동시에 실물 로봇으로 옮길 수 없는 유일한 줄이기도 합니다.

실제 로봇 앞에는 이 줄이 존재할 수 없습니다. 작업대 위 물건이 어디 있는지 알려 주는 사람이 없거든요. 로봇은 스스로 알아내야 합니다.

게다가 시뮬레이터가 알려 주는 좌표에는 오차가 없습니다. 밀리미터 단위까지 정확하죠. 카메라로 알아낸 위치라면 조명이 어둡거나 물건이 반짝이기만 해도 몇 센티미터씩 흔들립니다. 같은 코드가 실물에서 무너지는 이유의 상당 부분이 이 차이에서 나올 겁니다.

여기서 이 시리즈의 남은 길이 통째로 보였습니다. 앞으로 할 공부는 전부 그 한 줄을 무엇으로 대체하느냐의 문제였습니다.

손목에 카메라를 달고 그 줄을 인식 결과로 바꾸면 비전 기반 조작이 됩니다. 사람이 조종한 시연을 모아 학습시킨 신경망으로 바꾸면 모방학습이고요. "파란 블록을 옆으로 옮겨"라는 문장을 알아듣는 모델로 바꾸면 그게 요즘 말하는 VLA입니다.

세 갈래 전부 같은 자리를 노립니다. 로봇을 바꾸는 게 아니라 그 한 줄의 출처를 바꾸는 것. 배울 것이 많아 막막했는데, 끼워 넣을 구멍이 하나뿐이라는 걸 알고 나니 오히려 마음이 편해졌습니다.

맺음말

예제를 돌려 보는 것과 예제를 읽는 것은 완전히 다른 일이었습니다. 돌릴 때는 로봇이 주인공이었는데, 읽고 나니 주인공이 좌표 일곱 개더군요. 화면만 보고 있었다면 영영 몰랐을 겁니다.

그리고 시뮬레이션의 정체도 조금 분명해졌습니다. 시뮬레이터는 모든 정답을 알고 있는 세계입니다. 그래서 편하고, 그래서 위험합니다. 이 안에서 잘 도는 코드가 실물에서 무너지는 이유의 상당 부분이 그 한 줄에 있을 겁니다.

다음 글에서는 한 층 더 내려가 보겠습니다. "그 좌표로 가라"는 한 줄이 실제로 무엇을 계산하는지. 거기서 답이 없는 문제를 만났습니다.

참고 자료

  • NVIDIA Isaac Sim 소스 코드(아파치 2.0 라이선스로 공개) — 깃허브 저장소
  • 이 글에서 인용한 코드는 Isaac Sim 6.0 설치본의 두 파일입니다 — standalone_examples/api/isaacsim.robot.experimental.manipulators/franka/pick_place.py(실행용)와 exts/isaacsim.robot.experimental.manipulators.examples/.../franka/pick_place.py(내용물). 주석은 원문이 영어라 제가 한국어로 옮겼습니다.
  • Isaac Sim 예제와 API 설명 — 공식 문서
  • Franka Emika Panda — 예제에 쓰인 7축 로봇 팔 — 제조사 링크

이 블로그의 인기 게시물

피지컬 AI, 학계는 아직 이 단어를 쓰지 않습니다

로봇 감속기, 톱니 두 개가 100배를 만듭니다

서보모터는 가만히 있을 때 가장 뜨겁습니다