로봇은 왜 한 발로 넘어지지 않는가: CoM·support polygon·ZMP로 읽는 G1 균형

G1 보행 실험 v131에서 저는 무게중심을 지지발 쪽에 붙잡는 보상을 추가했습니다. 결과만 보면 어느 정도 효과가 있었습니다. 느린 명령에서 평균 에피소드 길이는 1.818초에서 2.296초로 늘었고, gait score도 0.869에서 0.910으로 좋아졌습니다. 그런데 로봇은 여전히 100% 넘어졌습니다. 더 이상한 점은 전체 질량으로 계산한 CoM 여유가 -0.335m에서 -0.411m로, ZMP 여유는 -5.473m에서 -6.503m로 오히려 나빠졌다는 것입니다.

처음에는 “CoM 보상이 약한가?”라고 생각하기 쉽습니다. 하지만 이 수치는 다른 질문을 던집니다. 제가 붙잡고 있던 값이 실제 동적 균형을 충분히 설명하는 값이었는가? 그리고 코드에서 support polygon과 ZMP라고 부른 값은 물리학에서 말하는 그것과 정말 같은가?

이 글은 균형 이론을 교과서 순서로 정리하는 글이 아닙니다. 현재 G1 학습 코드가 발 접촉 이후 어떤 값을 계산하는지, 그 값이 무엇을 말할 수 있고 무엇은 말할 수 없는지, 그리고 v130부터 v137까지의 실패가 다음 측정 설계를 어떻게 바꿨는지를 기록합니다.

G1 보행에서 CoM, 지지 영역, ZMP의 물리적 의미와 현재 진단 프록시 계산 절차 및 v130부터 v137까지의 실험 결과를 비교한 다이어그램
물리 개념과 현재 구현의 계약은 같지 않습니다. 지금의 trace는 양발 sole sample로 만든 축 정렬 박스와 LIPM 기반 ZMP 프록시를 사용하며, v130~v137의 값은 합격 판정이 아니라 실패 순서와 브랜치 비교에 사용합니다.

지지면은 발 모양이 아니라 ‘현재 닿아 있는 영역’입니다

로봇이 바닥에 서 있을 때 지면은 발에 수직항력과 마찰력을 가합니다. 이 접촉력이 만들어질 수 있는 평면상의 영역을 단순화한 것이 support polygon, 즉 지지 다각형입니다. 실제 계산에서는 현재 활성화된 접촉점들의 convex hull을 사용합니다.

양발이 모두 바닥에 닿아 있으면 왼발과 오른발의 접촉점을 함께 둘러싼 넓은 영역이 생깁니다. 한 발만 닿는 single-support 구간에서는 영역이 지지발 발바닥 크기로 급격히 줄어듭니다. 반대쪽 발은 공간상 가까이 있어도 접촉하지 않는 순간부터 지지면을 만들지 못합니다. 이 차이는 이전 글에서 정리한 발 접촉·마찰·single-support 판정이 균형 계산의 입력 계약이 되는 이유이기도 합니다.

따라서 support polygon을 계산하려면 최소한 다음 두 가지가 필요합니다.

  • 어느 발 또는 어느 sole sample이 실제로 접촉 중인지
  • 그 접촉점들이 월드 좌표계에서 어디에 놓였는지

발 링크의 중심 좌표만 보거나, 접촉하지 않은 swing foot까지 포함하면 지지면이 실제보다 넓어질 수 있습니다. 반대로 발바닥을 한 점으로만 취급하면 회전 모멘트를 견딜 수 있는 실제 면적을 잃습니다. 지지면은 시각화를 위한 사각형이 아니라, 접촉 모델이 허용하는 힘의 영역을 2차원으로 투영한 결과입니다.

CoM은 로봇의 질량이 어디에 모였는지를 말합니다

Center of Mass, CoM은 각 링크의 질량과 위치를 가중 평균한 점입니다. 링크 i의 질량을 m_i, 링크 CoM 위치를 p_i라고 하면 전체 CoM은 다음과 같습니다.

whole_body_com = sum(m_i * p_i) / sum(m_i)

현재 `debug_g1_motion_trace.py`도 Isaac Lab이 제공하는 body mass와 body CoM position을 이용해 이 값을 직접 계산합니다. 동시에 `root_com_pos_w`도 별도로 기록합니다. 둘을 나누어 기록한 이유는 root link의 CoM과 로봇 전체의 질량중심이 같은 값이 아니기 때문입니다. 팔을 휘두르고 다리를 들어 올리면 전체 CoM은 root link만 보았을 때와 다르게 움직입니다.

Isaac Lab의 articulation 데이터에는 `root_com_pos_w`, `body_com_pos_w`, `body_com_lin_vel_w` 같은 값이 구분되어 있습니다. 이름이 비슷하다고 섞어 쓰면 “골반 위치가 안정적이니 전체 질량도 안정적이다”라는 잘못된 결론에 도달할 수 있습니다. 이 구분은 world·body·joint frame을 구분한 글에서 다룬 좌표계 계약과도 연결됩니다.

CoM 투영점이 지지면 안에 있다는 것은 정적인 질문입니다

CoM을 바닥에 수직으로 투영한 점이 support polygon 안에 있으면 로봇은 정적으로 버틸 수 있는 자세에 가깝습니다. 아주 느리게 움직이고 관성 효과가 작다는 조건에서는 유용한 판정입니다. 그러나 걷는 로봇은 계속 가속하고, 팔과 다리의 각운동량도 바뀌며, 다음 발을 어디에 놓을지 결정합니다.

예를 들어 CoM 투영점이 아직 지지발 안에 있어도 몸이 바깥쪽으로 빠르게 움직이면 곧 영역을 벗어납니다. 반대로 순간적으로 투영점이 영역 밖에 있어도 swing foot을 적절한 위치에 내려놓으면 회복할 수 있습니다. 위치 하나만으로는 속도와 회복 가능한 발 위치를 설명하지 못합니다.

그래서 현재 trace의 `mass_com_margin_x`, `mass_com_margin_y`는 “넘어졌는가”를 직접 판정하는 값이 아닙니다. 현재 계산된 지지 영역 경계까지 남은 기하학적 거리를 뜻합니다. 양수면 박스 안, 음수면 박스 밖이라는 해석까지만 안전합니다.

ZMP는 합력의 작용 위치를 묻습니다

Zero-Moment Point는 지면과 평행한 축에 대한 순 모멘트가 0이 되는 지면상의 점입니다. 직관적으로는 로봇에 작용하는 중력과 관성력이 합쳐졌을 때, 그 결과 하중이 지면의 어디를 통과하는지를 묻습니다. ZMP가 유효한 지지 영역 안에 유지되면 발 전체가 들리거나 회전하지 않는 접촉 상태를 만들 수 있습니다.

고전적인 ZMP 정리3D Linear Inverted Pendulum Model은 이 관계를 보행 궤적 생성에 사용할 수 있게 만들었습니다. 일정한 CoM 높이와 작은 각운동량 같은 LIPM 가정 아래, 한 축의 ZMP는 다음처럼 근사할 수 있습니다.

zmp_x = com_x - (com_height / gravity) * com_acc_x
zmp_y = com_y - (com_height / gravity) * com_acc_y

CoM 위치만 보던 판정에 가속도 항이 추가됩니다. 같은 위치에서도 바깥쪽 가속도가 크면 ZMP는 더 빠르게 경계로 이동합니다. 다만 이 식은 어디까지나 모델 가정 아래의 결과입니다. 실제 접촉력 센서로 CoP를 구한 값, 전신 동역학과 각운동량을 포함한 ZMP, 단순 LIPM 근사값은 구분해서 불러야 합니다.

회복 가능성을 보려면 속도와 다음 발 위치까지 필요합니다

CoM projection은 위치를, ZMP는 위치와 가속도의 관계를 보여줍니다. 그러나 “지금 이 상태에서 한 걸음으로 회복할 수 있는가?”에는 CoM 속도와 다음 발을 놓을 수 있는 영역이 추가로 필요합니다. 이 문제를 간결하게 표현하는 개념이 capture point입니다.

높이가 일정한 LIPM에서는 capture point를 대략 `CoM 위치 + CoM 속도 / 고유진동수`로 해석할 수 있습니다. 몸이 빠르게 오른쪽으로 흐르고 있다면 현재 CoM 바로 아래가 아니라 더 오른쪽에 발을 놓아야 멈출 수 있다는 뜻입니다. 현재 G1 학습에 capture point 제약을 넣었다는 뜻은 아닙니다. CoM 여유만 개선됐는데도 로봇이 계속 넘어졌던 이유를 해석하기 위한 다음 관측 후보입니다.

답하는 질문 필요한 입력 현재 G1에서의 상태
CoM 전체 질량이 어디에 있는가 링크별 질량과 CoM 위치 root CoM과 mass-weighted CoM을 trace에 기록
CoM projection margin 정적 지지 영역에서 얼마나 떨어졌는가 CoM과 지지 영역 축 정렬 박스 기준 진단값
ZMP / CoP 결과 하중이 지면의 어디를 통과하는가 동역학 또는 접촉 wrench LIPM 식 기반 프록시만 기록
Capture point 한 걸음으로 어디에 발을 놓아야 멈출 수 있는가 CoM 위치·속도와 모델 높이 아직 구현·평가하지 않음

현재 trace의 ‘support polygon’은 사실 축 정렬 박스입니다

여기서부터가 현재 구현의 경계입니다. `debug_g1_motion_trace.py`는 발마다 heel, toe, 좌우 가장자리 등 5개의 sole sample을 만들고 월드 좌표계로 변환합니다. 그 다음 열 점 전체의 x와 y 최솟값·최댓값을 취합니다.

support_min_x = min(all_sole_points_x)
support_max_x = max(all_sole_points_x)
support_min_y = min(all_sole_points_y)
support_max_y = max(all_sole_points_y)

margin_x = min(com_x - support_min_x,
               support_max_x - com_x)

계산이 빠르고 브랜치 비교에는 편리합니다. 하지만 엄밀한 support polygon은 아닙니다.

첫째, 현재 코드는 접촉 여부로 sample을 거르지 않습니다. single-support 중에도 공중의 swing foot 좌표가 박스에 포함됩니다. 둘째, convex hull이 아니라 월드 축에 정렬된 AABB를 사용합니다. 발이 회전했거나 두 발이 비스듬히 놓이면, 사각형 모서리 일부는 실제 convex hull 바깥인데도 지지 영역으로 포함됩니다. 따라서 현재 margin이 양수라고 해서 실제 접촉 다각형 안에 있다는 보장은 없습니다.

이 값은 삭제할 대상이 아니라 이름과 용도를 제한해야 할 진단 프록시입니다. 같은 코드와 같은 명령으로 학습 브랜치를 비교하거나, 어느 신호가 먼저 무너지는지 볼 때는 유용합니다. 정확한 안정 여유를 주장하거나 제어 제약으로 바로 사용할 값은 아닙니다.

현재 ZMP도 측정값이 아니라 LIPM 프록시입니다

trace는 root linear velocity를 policy step 간 차분해 root acceleration을 만들고, 앞의 LIPM 식에 넣습니다. mass-weighted CoM 위치를 사용하는 `mass_zmp`도 가속도는 mass CoM 가속도가 아니라 root acceleration을 공유합니다.

root_acc = (root_velocity - previous_root_velocity) / policy_dt
mass_zmp = mass_com - (mass_com_height / 9.81) * root_acc

이 구현에는 세 가지 분명한 한계가 있습니다.

  • 유한차분한 root velocity라서 접촉 충격과 한 step의 잡음에 민감합니다.
  • 전체 질량중심을 사용하면서 가속도는 root link 값으로 대신합니다.
  • 각운동량 변화와 발별 contact wrench를 사용하지 않습니다.

그래서 -6.503m 같은 큰 음수값을 실제 force plate에서 측정한 ZMP가 발에서 6m 벗어났다고 읽으면 안 됩니다. 이 값은 해당 rollout에서 LIPM 가정이 크게 깨졌거나 차분 가속도가 급격했다는 경고입니다. 같은 trace 안에서 사건 순서와 실험 간 경향을 비교하는 용도가 적절합니다.

v87은 균형 지표보다 액추에이터 포화가 먼저였다고 말합니다

진단값의 가장 유용한 사용법은 “어떤 사건이 먼저 발생했는가”를 보는 것입니다. v87 zero-policy 진단에서 16개 환경 모두가 평균 0.020초에 torque limit ratio 0.95를 넘었습니다. root-CoM margin은 평균 0.599초, pitch 0.2rad는 0.770초, mass-CoM margin은 2.100초에 음수가 됐습니다. 최초 termination은 평균 2.684초였습니다.

대표 env 0에서도 0.02초에 발목 torque ratio가 0.994와 1.000에 도달했지만 mass-CoM margin은 +0.073m, mass-ZMP margin은 +0.058m였습니다. pitch가 0.206rad가 된 0.78초에도 CoM margin은 +0.068m였습니다. 이 순서는 당시 실패를 “CoM이 먼저 지지면을 벗어나서 넘어졌다”라고 설명할 수 없게 만듭니다. 액추에이터 설정과 초기 제어가 먼저 무너졌고, 균형 지표 악화는 뒤따른 현상이었습니다.

반대로 official asset과 actuator 설정으로 바꾼 v88에서는 torque 포화 없이 mass-ZMP margin이 0.04~0.06초, mass-CoM margin이 0.56~0.58초에 먼저 음수가 됐습니다. 같은 낙상이라도 원인 순서가 달라졌습니다. 이 때문에 단일 최솟값보다 이벤트 타임라인이 더 많은 정보를 줍니다.

v131의 역설: 더 오래 버텼지만 동적 여유는 나빠졌습니다

v130은 swing foot의 과도한 상승을 억제하는 상태였습니다. 느린 명령에서 평균 에피소드 1.818초, 이동거리 1.027m, single-support ratio 0.6093을 기록했지만 모두 넘어졌습니다. trace의 최소 mass-CoM margin은 -0.335m, mass-ZMP margin은 -5.473m였습니다.

v131에서는 single-support 동안 CoM의 lateral offset이 0.16m를 넘으면 제곱 패널티를 주었습니다. 평균 에피소드는 2.296초로 늘고 gait score도 좋아졌습니다. 하지만 최소 CoM과 ZMP 프록시는 각각 -0.411m와 -6.503m로 악화됐습니다.

정적 offset 패널티가 자세를 잠시 붙들어 생존 시간을 늘렸지만, 옆으로 흐르는 속도와 착지 위치를 직접 해결하지 못한 것으로 해석했습니다. 여기서 “에피소드가 길어졌으니 균형이 개선됐다”라는 판정을 멈췄습니다. 생존 시간, 지지면 여유, 속도, 좌우 대칭, 실제 이동 방향을 별도 gate로 분리해야 했습니다.

실험 추가한 관점 느린 명령 결과 최소 CoM / ZMP 프록시 판정
v130 과상승 억제 1.818초, fall 100% -0.335m / -5.473m 기준선
v131 정적 CoM offset 2.296초, fall 100% -0.411m / -6.503m 생존만 개선
v132 lateral velocity 3.121초, fall 100% -0.307m / -4.543m 동적 경향 개선, 대칭 저하
v137 escape guard 누적 fall 43.75% -0.203m / -2.077m 프록시 개선, 보행 불합격

v132부터는 위치가 아니라 ‘바깥으로 흐르는가’를 보기 시작했습니다

v132에서는 body lateral velocity가 deadband를 넘을 때 패널티를 주었습니다. 평균 에피소드는 3.121초로 늘었고 CoM과 ZMP 프록시도 v131보다 덜 나빠졌습니다. 하지만 이동거리는 0.697m로 줄었고 좌우 지지 대칭이 깨졌습니다. 몸을 옆으로 보내지 않는 정책이 앞으로 걷는 정책과 같지는 않았습니다.

v135에서는 지지발 바깥쪽으로 이미 치우친 상태에서 속도까지 같은 방향인 `escape` 조건을 분리했고, 이후 continuation을 거쳐 v137에서 느린 명령 낙상률은 43.75%, 최소 CoM margin은 -0.203m, ZMP 프록시는 -2.077m까지 개선됐습니다. 그래도 nominal 명령 낙상률은 84.375%였고 step asymmetry는 0.886이었습니다. trace에서는 왼발 off 이벤트 662회, 오른발 163회로 편향도 컸습니다.

프록시 추세는 좋아졌지만 checkpoint를 채택하지 않았습니다. 넘어지지 않는 시간만 늘어난 비대칭 hopping을 보행 성공으로 부를 수 없기 때문입니다. 이는 reward hacking을 판별하는 기준과 같은 문제입니다. 한 지표를 직접 최적화하면 정책은 그 지표를 만족하는 가장 싼 동작을 찾습니다.

현재 RL 정책은 정확한 ZMP를 관측하거나 제약하지 않습니다

현재 `g1_walk_env.py`의 정책 관측은 73차원입니다. base linear/angular velocity, projected gravity, command, gait phase와 support side, joint position/velocity, previous action으로 구성됩니다. 명시적인 support polygon vertex, contact wrench, CoP, ZMP는 관측에 없습니다.

환경에는 single-support 시 지지발 y좌표와 root CoM 사이의 lateral offset, body lateral velocity, 바깥쪽 escape를 계산하는 코드가 남아 있습니다. 다만 현재 기본 reward profile인 `compact_v344_inertia_wbo_swing_lane`이 과거 v131~v137의 모든 항을 그대로 활성화한다는 뜻은 아닙니다. 실험 보고서의 구버전 결과와 현재 기본 profile의 실행 계약을 섞으면 재현할 수 없는 설명이 됩니다.

현재 정책이 실제로 받는 것은 action에서 PD target과 torque로 이어지는 신호입니다. 균형은 관측·reward·액추에이터·접촉의 결합 결과이지, ZMP controller가 바깥에서 정책을 교정하는 구조가 아닙니다.

WBC-lite에는 CoM planner가 있지만 ZMP 제어기는 아직 아닙니다

`sim/wbc/com_planner.py`는 LIPM 기반의 간단한 CoM planner를 구현합니다. stance foot이 있으면 그 위치들의 평균을 CoP로 사용하고, `omega = sqrt(g / h)`와 CoM-CoP 오차로 수평 가속도 목표를 만듭니다. 지지발 수에 따라 원하는 지면반력을 균등 분배합니다.

하지만 현재 구현은 contact polygon 안의 ZMP 궤적을 계획하거나 contact wrench 제약을 푸는 전신 제어기가 아닙니다. `wbc_qp.py`도 floating-base equation of motion, mass matrix, 마찰원뿔, 접촉 wrench를 함께 푸는 QP가 아니라 joint target을 bounds 안에서 맞추는 선택적 SLSQP와 PD·근사 중력보상 구조입니다. exact RNEA는 아직 구현되지 않았습니다.

`eval_wbc.py`는 CoM planner의 정지·단일 지지 GRF·높이 오차와 scheduler 결합을 합성 입력에서 검사합니다. 이것은 컴포넌트 검증이지 Isaac Sim에서 G1이 폐루프로 걸었다는 증거가 아닙니다. 현재 상태를 WBC와 RL의 역할 경계로 표현하면 다음과 같습니다.

단계 현재 상태 근거
설계됨 CoM·지지면·동적 여유를 분리해 측정 trace field와 실험 판정표
구현됨 mass-weighted CoM, AABB margin, LIPM ZMP proxy `debug_g1_motion_trace.py`
실행됨 v87~v137 및 최신 브랜치 trace experiment report
관측됨 이벤트 순서와 reward별 프록시 경향 정량 rollout 결과
채택됨 없음 비대칭·경로·낙상 gate 미통과
배포됨 없음 router-ready checkpoint 없음

최신 v356도 ‘안 넘어짐’과 ‘제대로 걸음’이 다르다고 보여줍니다

최신 보고서의 v356은 느린 명령에서 낙상률 12.5%, nominal에서 9.375%까지 낮아졌습니다. 과거 v137보다 생존성은 크게 좋아졌습니다. 그러나 nominal 명령에서 평균 displacement는 -0.806m로 목표 방향과 반대였고, trace의 lateral CoM margin은 평균 +0.0487m지만 최소 -0.1253m였습니다. ZMP 프록시 margin은 평균 -0.3059m, 최소 -2.758m였습니다.

더구나 feet crossing 0.308, 최소 foot separation -0.235m, 팔 포화와 형태 이상이 남아 있었습니다. 그래서 v356도 router-ready로 채택하지 않았습니다. 균형 지표는 필요하지만 충분하지 않습니다. 명령 추종, 진행 방향, 발 교차, 좌우 대칭, 형태, actuator margin을 함께 통과해야 제가 원하는 보행입니다.

다음 구현은 reward가 아니라 측정 계약부터 바꿉니다

v131에서 바로 패널티 계수를 키웠다면 같은 시행착오를 반복했을 가능성이 큽니다. 다음 단계는 reward를 추가하기 전에 진단값의 물리적 계약을 강화하는 것입니다.

  1. 실제 contact bool 또는 contact sensor로 활성 sole sample만 남깁니다.
  2. 활성 접촉점의 2D convex hull을 만들고 AABB가 아닌 polygon signed distance를 계산합니다.
  3. 발별 contact wrench에서 CoP를 계산하고 현재 LIPM ZMP 프록시와 같은 timestamp에서 비교합니다.
  4. root acceleration 대신 mass-CoM acceleration을 기록하고, 차분 방식과 필터 지연을 명시합니다.
  5. CoM velocity와 feasible foot placement를 이용한 capture point 계열 지표를 offline trace에 추가합니다.
  6. Isaac Sim 화면의 접촉점·지지 다각형·CoM·CoP를 같은 프레임에 겹쳐 수치와 시각을 대조합니다.

그 뒤에야 “어떤 지표를 observation이나 reward에 넣을 것인가”를 결정할 수 있습니다. 측정값이 swing foot을 포함하고 root acceleration에 의존하는 상태에서 reward만 정교하게 만들면, 정책은 물리적 균형이 아니라 프록시의 빈틈을 학습합니다.

이번 실험에서 얻은 결론은 단순합니다. CoM은 질량의 위치를, support polygon은 현재 접촉이 허용하는 영역을, ZMP는 동역학이 만든 결과 하중의 위치를 말합니다. 셋은 연결돼 있지만 서로 대체할 수 없습니다. 그리고 현재 G1 코드가 가진 것은 완성된 ZMP 안정화 제어기가 아니라, 실패의 순서를 읽기 위한 진단 계기판입니다. 다음 checkpoint를 채택하기 전에 먼저 그 계기판을 실제 접촉과 일치시키는 일이 남아 있습니다.

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤