카메라가 “빨간 상자가 내 앞 0.5미터에 있다”고 말했어요. 그런데 팔은 그 말을 그대로 쓸 수 없어요. 팔은 자기 어깨를 기준으로 움직이거든요.
로봇 부품마다 자기 기준점이 있고, 그 사이를 옮겨 다니는 체계가 좌표 변환이에요. 게임 엔진의 부모-자식 계층과 하는 일이 같은데, 두 가지가 다르고 그 둘이 이 글의 전부예요.
기준점마다 이름이 있어요
좌표계는 기준점 하나를 말해요. 로봇 쪽에는 표준 이름이 정해져 있어서, 처음 보는 로봇이라도 이름만으로 무엇의 기준인지 알 수 있어요.
- 전역 지도의 원점
- 주행 거리를 누적한 기준
- 로봇 몸통의 중심
- 카메라 렌즈의 광학 중심
- 그리퍼 끝
이들은 부모-자식 트리로 이어지고, 각 간선이 “부모에서 자식으로 얼마나 이동하고 회전했는가”를 담아요. 임의의 두 좌표계 사이 변환은 트리 경로를 따라 곱해서 나와요. 지도 기준으로 본 그리퍼 위치는 지도에서 몸통까지, 몸통에서 팔 관절들을 지나 그리퍼까지의 변환을 차례로 누적한 결과예요.
여기까지는 게임 엔진의 계층 구조와 똑같아요. 다른 점 둘이 뒤에 나와요.
계층이 프로세스를 넘어 흩어져 있어요
첫 번째 차이예요. 게임 엔진에서는 계층 전체가 한 프로세스 안에 있어서 부모를 타고 올라가면 언제든 조회할 수 있어요.
로봇에서는 각 노드가 자기가 아는 구간만 발행해요. 위치 추정 노드가 지도에서 주행 기준까지를, 주행 계산 노드가 거기서 몸통까지를, 관절 상태를 읽는 노드가 몸통에서 각 링크까지를 각각 내보내요. 트리는 이 조각들이 합쳐져야 완성돼요.
그래서 새로운 실패 모드가 생겨요. 한 노드가 죽으면 트리가 그 지점에서 끊기고, 그 너머의 변환 조회가 전부 실패해요. 증상은 “지도 기준 위치를 못 구한다”인데 원인은 전혀 다른 노드에 있어요.
조회가 실패했을 때 던져지는 메시지가 대개 **“어느 좌표계에서 어느 좌표계로 가는 경로가 없다”**는 형태예요. 이걸 보면 코드를 보기 전에 누가 그 구간을 발행해야 하는지부터 확인해요. 트리를 그림으로 뽑아 주는 도구가 있어서 끊긴 자리가 눈으로 보여요.
3초 전의 변환이 필요해요
두 번째 차이가 더 중요해요. 로봇은 움직여요.
카메라가 3초 전에 찍은 프레임에서 물체를 찾았다고 해요. 그동안 로봇이 움직였다면 3초 전의 카메라 위치와 지금의 카메라 위치가 달라요. 그 물체가 지도 상 어디에 있는지 알려면 지금의 변환이 아니라 3초 전의 변환을 써야 해요.
그래서 변환을 타임스탬프와 함께 버퍼에 쌓아 두고, 조회할 때 시각을 지정해요. 이게 좌표 변환이 단순한 행렬 곱이 아닌 이유예요.
여기서 실전 함정 둘이 나와요.
버퍼보다 오래된 시각은 조회할 수 없어요. 버퍼는 유한한 시간만큼만 들고 있어서, 처리가 밀려 오래된 프레임을 뒤늦게 조회하면 이미 버려진 뒤예요. 증상은 “잘 되다가 부하가 걸리면 변환 실패가 뜬다”예요.
아직 안 온 시각도 조회할 수 없어요. 조회하려는 시각의 변환이 아직 발행되지 않았으면 실패해요. 그래서 조회에는 대기 시간을 주는 형태가 따로 있어요. 지정한 시간만큼 기다렸다가 그래도 없으면 실패하는 방식이죠. 대기 없이 조회하면 타이밍에 따라 되기도 하고 안 되기도 하는 증상이 나와요.
가장 최신 값을 달라는 특수한 지정도 있는데, 편하지만 정확히 언제의 값인지 모르게 돼요. 측정이나 대조가 목적이면 시각을 명시하는 쪽을 써요.
구조를 적어 두면 변환이 공짜로 생겨요
로봇의 링크와 관절 구조를 적는 형식이 있어요. 각 링크의 크기와 무게, 관절이 어느 축으로 얼마나 돌 수 있는지를 담죠.
이걸 읽는 노드가 하나 있는데, 관절 각도만 받으면 거기서 모든 링크의 변환을 계산해 발행해요. 그래서 구조 파일만 제대로 있으면 팔의 모든 좌표계가 저절로 생겨요. 링크를 하나 추가해도 코드를 안 고쳐도 되고요.
역으로, 구조 파일의 값이 틀리면 변환이 조용히 틀려요. 관절 축 방향이 반대로 적혀 있으면 팔이 반대로 도는 것으로 계산되는데, 시뮬레이터에서는 그럴듯하게 움직이니 발견이 늦어요. 실물과 대조해야 드러나죠.
관절 회전을 실제로 어떤 수로 적는지, 왜 각도 셋이 아니라 넷을 쓰는지는 회전 표현 — 오일러는 UI, 쿼터니언은 데이터에 있어요.
정리하면
좌표 변환의 어려움은 개념이 아니라 분산과 시간에 있어요. 계층이 여러 노드에 흩어져 있어서 한 곳이 죽으면 먼 곳의 조회가 실패하고, 시간축이 있어서 언제의 변환인지를 항상 신경 써야 해요.
그래서 실패했을 때 물어볼 것도 정해져 있어요. 경로가 끊겼는가, 아니면 시각이 범위 밖인가. 앞의 것은 누가 발행해야 하는지의 문제이고, 뒤의 것은 버퍼 길이와 대기 시간의 문제예요. 둘은 고치는 방법이 완전히 달라요.
기록에서 시간축을 다루는 이야기는 rosbag2 — 시계 축이 어긋난 기록은 조용히 틀려요에, 관절 명령이 실제로 나가는 경로는 ros2_control — update()의 주기 계약에 있어요.