휴머노이드의 한 걸음은 어떻게 만들어지는가: gait scheduler부터 WBC QP까지

G1 WBC-lite 보행 목표 생성기를 10초 동안 돌리면 왼발 12회, 오른발 13회의 교대 스텝이 나옵니다. 좌우 스텝 수 비대칭은 0.08이고, 전체 시간의 30%는 두 발 지지, 70%는 한 발 지지입니다. 최대 관절 목표 편차는 0.273rad, 최대 토크 대푯값은 21.89Nm였습니다. 배열의 모양도 맞고, 관절 순서도 맞고, 값이 제한을 벗어나지도 않았습니다.

이 목표를 Isaac Lab의 G1에 연결하자 결과는 전혀 달랐습니다. 느린 전진 명령에서는 0.484m 이동한 뒤 1.16초 만에 모든 환경이 횡방향 속도 조건으로 종료됐습니다. 정지 명령에서는 전진·후진 표류를 -0.019m/s까지 줄였지만 몸체 기울기가 0.781rad까지 커졌고 1.24초에 종료됐습니다. 코드가 스텝처럼 보이는 숫자를 만든 것과 로봇이 실제로 한 걸음을 버틴 것은 다른 사건이었습니다.

그래서 이번 글에서는 WBC를 수식 한 줄로 소개하지 않습니다. 속도 명령 하나가 들어왔을 때 상태 추정기, gait scheduler, 발 궤적, CoM planner, 관절 목표 계산, PD actuator가 어떤 값을 주고받는지 현재 구현을 따라가 봅니다. 동시에 공통 WBC 모듈에 구현된 기능과 G1 보행기에 실제로 연결된 기능을 나눠, 어디까지가 구현이고 어디부터가 다음 검증인지 표시합니다.

G1 WBC-lite에서 속도 명령이 상태 추정, 보행 위상, 발과 CoM 목표, 관절 목표, PD 토크를 거쳐 다시 측정 상태로 돌아오는 한 걸음 폐루프 파이프라인
한 걸음은 단일 함수가 아니라 서로 다른 시간과 책임을 가진 여러 단계의 합입니다. 현재 코드는 단계별 출력의 일관성은 확인했지만, Isaac 폐루프에서 안정적인 보행까지 통과하지는 못했습니다.

한 걸음은 발을 앞으로 보내는 함수 하나가 아닙니다

걷는 동작을 관절각 곡선으로만 보면 무릎을 굽혔다 펴는 주기 운동처럼 보입니다. 하지만 제어기 입장에서 한 걸음은 최소 다섯 가지 질문에 순서대로 답하는 과정입니다.

  1. 지금 몸체는 어느 방향으로 기울고 얼마나 빠르게 움직이는가?
  2. 어느 발이 지지발이고 어느 발이 공중에 있어야 하는가?
  3. 공중의 발은 어디를 거쳐 어디에 착지해야 하는가?
  4. 몸의 무게중심과 지면반력 목표를 어떻게 잡을 것인가?
  5. 그 목표를 29개 관절의 위치 또는 토크로 어떻게 바꿀 것인가?

이 가운데 하나라도 시간축이나 좌표계가 어긋나면 나머지 단계가 올바른 숫자를 내더라도 로봇은 넘어집니다. 예를 들어 scheduler는 왼발이 stance라고 말하는데 접촉 센서는 이미 왼발이 떨어졌다고 보고할 수 있습니다. 이때 planner가 왼발을 지지점으로 사용하면 CoM 목표와 지면반력 배분은 처음부터 잘못된 전제 위에 놓입니다.

1단계: StateEstimator는 센서값을 제어기가 읽을 상태로 바꿉니다

sim/wbc/state_estimator.py의 입력은 관절 위치·속도, IMU 가속도·각속도·quaternion, 발 접촉 신호입니다. 출력은 base 위치·자세·선속도·각속도, Euler 각, 관절 상태, 이진 접촉 상태를 묶은 상태 사전입니다. 이후 CoM planner와 WBC가 센서별 형식을 몰라도 같은 상태 계약을 읽게 하려는 계층입니다.

현재 구현은 contact-aided EKF가 아닙니다. quaternion으로 중력 방향을 제거한 IMU 가속도를 적분하고, 속도에 0.85 계수의 저역통과 필터를 적용합니다. 발 접촉력은 10N을 넘으면 접촉으로 분류합니다. 코드의 설명처럼 시뮬레이션용 경량 추정기이며, 적분 드리프트와 접촉을 이용한 base 위치 보정은 남아 있습니다.

sensor packet
  -> quaternion orientation
  -> gravity-removed acceleration
  -> filtered base velocity and integrated position
  -> contact_force > 10 N
  -> state {base_euler, base_lin_vel, q, dq, contact}

평가 파일에는 identity quaternion에서 Euler 각이 0인지, 90도 roll이 검출되는지, 10N 임계값과 bool 접촉 입력이 처리되는지, reset이 기준 높이를 복원하는지가 기록돼 있습니다. 이것은 상태 사전의 계산 계약을 확인한 결과입니다. 장시간 보행에서 위치 추정 오차가 작다는 증거는 아닙니다.

2단계: GaitScheduler는 연속 시간을 접촉 모드로 자릅니다

GaitScheduler는 내부 위상 시계 phase_clock를 0에서 1까지 진행시킵니다. WALK 설정은 주기 0.8초, duty factor 0.65, 오른발 위상 오프셋 0.5입니다. 각 발의 국소 위상이 0.65보다 작으면 stance, 그 이후면 swing입니다. swing 구간에서는 위상을 다시 0에서 1로 정규화해 발 궤적 생성기에 넘깁니다.

phase = (global_phase + foot_offset) mod 1
contact = phase < duty
swing_phase = (phase - duty) / (1 - duty)   # swing일 때만 0 -> 1

두 발의 offset이 0.5인 대칭 보행에서 double-support 비율은 max(0, 2 × duty - 1)입니다. 따라서 duty 0.65이면 한 주기의 30%가 두 발 지지이고, swing 시간은 0.8 × (1 - 0.65) = 0.28초입니다. 저장된 평가 결과에서도 WALK의 double support는 0.30, 왼발·오른발 stance 비율은 각각 0.664와 0.636으로 측정됐습니다.

이 scheduler의 장점은 접촉 순서를 결정적으로 재현할 수 있다는 점입니다. 반대로 가장 큰 한계는 시간표가 실제 접촉을 보장하지 않는다는 점입니다. 발이 예상보다 일찍 닿거나 늦게 떨어져도 phase clock은 계속 갑니다. 따라서 다음 단계에서는 발 접촉 신호와 support phase를 구분한 기록처럼 예정 접촉과 측정 접촉을 함께 남겨야 합니다.

3단계: FootTrajectory는 swing 발의 시작점과 착지점을 잇습니다

FootTrajectory는 시작 위치 p_start, 착지 목표 p_end, 0에서 1 사이의 swing phase를 받아 cubic Bezier 곡선으로 위치를 만듭니다. 양쪽 내부 제어점을 지면보다 높게 두어 중간에 발을 들어 올리고, 미분한 곡선을 swing duration으로 나눠 목표 속도도 계산합니다.

p(t) = (1-t)^3 p0 + 3(1-t)^2 t p1 + 3(1-t)t^2 p2 + t^3 p3
v(t) = dp/dt / swing_duration

중간 제어점의 z를 단순히 step_height만큼 올리면 cubic Bezier의 중앙 높이는 그 값의 75%만 올라갑니다. 그래서 구현은 제어점 높이에 4/3을 곱해 실제 중앙 최고점이 설정한 발 들기 높이와 맞도록 했습니다. 단위 테스트는 시작점, 끝점, 최고 높이, xy 보간 범위, 속도 방향을 각각 확인합니다.

착지점은 Raibert 방식의 간단한 휴리스틱으로 현재 발 위치에 v_cmd × T/2를 더합니다. 명령 속도가 클수록 다음 발을 더 멀리 내딛는 구조입니다. 다만 현재 식에는 실제 속도 오차나 capture point, hip projection이 명시적으로 들어가지 않습니다. 함수 주석은 Raibert-style이라고 적혀 있지만, 지금 구현은 그 아이디어의 최소 형태에 가깝습니다.

공통 발 궤적 모듈과 G1 전용 보행 경로는 아직 완전히 연결되지 않았습니다

여기서 코드 구조를 꼼꼼히 구분해야 합니다. FootTrajectory는 3차원 발 위치와 속도를 만들 수 있습니다. 하지만 현재 G1WBCWalkingController의 실제 step()은 이 발 위치를 다리 역기구학으로 풀어 관절 목표를 만들지 않습니다. 생성자에서 FootTrajectory를 보유하지만, 보행 목표 계산에서는 scheduler의 swing phase를 sin 함수에 넣어 hip pitch, knee, ankle pitch·roll, hip roll·yaw를 직접 변형합니다.

lift  = sin(pi * swing_phase)
sweep = sin(0.5 * pi * swing_phase)

hip_pitch  += stride-dependent sweep
knee       += stride-dependent lift
ankle_pitch -= stride-dependent lift

이 선택 덕분에 Pinocchio나 전신 역기구학 없이도 29차원 관절 목표를 빠르게 만들 수 있었습니다. 대신 “발이 5.5cm 올라간다”는 task-space 목표와 실제 발 링크가 5.5cm 올라갔다는 결과 사이에는 보장이 없습니다. 관절 위상 곡선은 매끄러워도 관절 결합, base 움직임, 접촉 때문에 발끝 궤적은 달라질 수 있습니다. 순수 Python 검사가 통과한 뒤 Isaac에서 곧바로 무너진 이유를 분석할 때 가장 먼저 확인해야 할 경계입니다.

4단계: CoMPlanner는 지지발을 기준으로 몸이 가속할 방향을 만듭니다

공통 CoMPlanner는 Linear Inverted Pendulum Model(LIPM)을 사용합니다. stance로 분류된 발 위치의 평균을 CoP 대푯값으로 두고, 현재 CoM 위치와 그 점의 차이, 명령 속도와 현재 속도의 차이로 수평 가속도 목표를 계산합니다. 고유진동수는 sqrt(g / h_nom)입니다.

omega = sqrt(g / nominal_height)
com_acc_xy = omega^2 * (com_xy - cop_xy)
             + kv * (velocity_command_xy - com_velocity_xy)

수직 방향은 기준 CoM 높이에 대한 P 제어를 사용합니다. 원하는 지면반력은 총중량 mass × g을 stance 발 수로 똑같이 나눕니다. 두 발 지지라면 절반씩, 한 발 지지라면 한 발이 전부 받는다는 목표입니다. 3D-LIPM의 기본 관계를 작은 planner로 옮긴 셈입니다.

이 계산에도 분명한 경계가 있습니다. stance 발 평균은 압력 센서로 측정한 실제 CoP가 아닙니다. 좌우 발의 하중이 다르거나 발바닥 안에서 압력 중심이 이동해도 반영되지 않습니다. 원하는 GRF를 계산하지만 현재 G1 전용 target shaper는 그 힘을 접촉 제약으로 풀어 쓰지 않습니다. CoM·support polygon·ZMP 진단 글에서 이 값을 물리 측정과 분리해 부른 이유가 여기에 있습니다.

5단계: WbcQP는 여러 목표를 관절 목표로 압축합니다

공통 WbcQP는 CoM 높이, base pitch·roll, swing 발목, nominal posture 목표를 하나의 q_des로 합칩니다. SciPy가 있으면 SLSQP로 가중 제곱오차를 최소화하고 관절 한계 안에서 해를 찾습니다. 가중치는 무릎 높이 50, hip pitch 30, hip roll 20, swing ankle 15, 전체 자세 정규화 5입니다. SciPy가 없으면 같은 의도를 관절별 보정식으로 적용하는 heuristic 경로를 사용합니다.

태스크 현재 관절 매핑 역할
CoM 높이 좌우 knee 기준 높이 오차를 무릎 굽힘으로 보정
base pitch 좌우 hip pitch 앞뒤 기울기 보정
base roll 좌우 hip roll 좌우 기울기 보정
swing clearance swing 쪽 ankle pitch 공중 발목을 기준 자세에서 -0.15rad 굽힘
posture regularization 전체 관절 해가 nominal pose에서 과도하게 멀어지는 것을 억제

이름에 QP가 들어가지만 지금 구현을 완전한 floating-base rigid-body dynamics QP로 설명하면 안 됩니다. 질량행렬 M(q), 코리올리·중력항 h(q,dq), 접촉 자코비안, 마찰콘을 하나의 hard constraint로 동시에 풀지 않습니다. friction_ok() 검사는 존재하지만 최적화 제약식 안에 연결돼 있지 않습니다. 정확한 RNEA 중력보상도 아니며 hip·knee에 대한 segment 기반 근삿값을 더합니다. Atlas에 사용된 optimization-based whole-body control과 같은 범주의 완성도를 주장하지 않고 WBC-lite라고 부르는 이유입니다.

마지막 단계: 관절 목표는 PD와 토크 제한을 거쳐서야 물리에 들어갑니다

q_des가 만들어졌다고 관절이 그 위치에 도달한 것은 아닙니다. 공통 WBC 경로는 다음 PD 식에 근사 중력보상을 더하고, 관절별 최대 토크로 자릅니다.

tau = Kp * (q_des - q) + Kd * (0 - dq) + tau_gravity_approx
tau = clip(tau, -tau_max, tau_max)

G1 전용 보행기는 동일한 취지로 29개 관절의 q_des와 토크 대푯값을 함께 만듭니다. hip·knee에는 더 큰 Kp를 적용하고, 목표 편차를 nominal pose 기준 ±0.45rad로 제한합니다. 이 단계의 실제 거동은 position·velocity·torque와 PD actuator 글에서 정리한 것처럼 gain, effort limit, action 적용 주기, articulation 설정에 따라 달라집니다.

그리고 물리를 한 번 진행한 뒤에는 계획했던 상태가 아니라 실제로 측정된 q, dq, base 자세, 접촉을 다시 첫 단계에 넣어야 합니다. 이 환류가 닫혀야 제어 루프입니다. 목표 배열만 반복 생성하거나, 목표를 일정 비율로 따라가는 1차 surrogate를 쓰는 검사는 폐루프 보행 검사가 아닙니다.

순수 Python 검사는 무엇을 통과시켰는가

eval_g1_wbc_controller.py는 10초, 20ms 간격, 전진 명령 0.25m/s 조건에서 500개의 목표를 만듭니다. plant 대신 현재 관절이 목표의 25%씩 따라가고 base 속도가 명령 속도로 수렴하는 간단한 surrogate를 둡니다. 여기서 확인한 것은 출력 차원, 관절 순서, 목표 편차 제한, 유한한 토크, 토크 제한, 좌우 교대, double-support 비율, 스텝 간 부드러움입니다.

측정값 결과 해석 가능한 범위
왼발 / 오른발 스텝 12 / 13 scheduler가 양쪽 swing을 교대로 생성
스텝 수 비대칭 0.08 10초 구간의 좌우 횟수가 유사
double / single support 0.30 / 0.70 duty 0.65의 예정 접촉 시간표와 일치
평균 air time 0.28초 설정된 swing duration과 일치
최대 관절 목표 편차 0.273rad ±0.45rad 제한 안에 있음
최대 토크 대푯값 21.89Nm 설정한 관절별 제한 안에 있음

이 결과는 target generator의 인터페이스 검증으로는 유용합니다. 특히 잘못된 관절 순서, 위상 불연속, 한쪽 발만 반복되는 오류, 갑자기 튀는 목표를 Isaac 실행 전에 걸러낼 수 있습니다. 그러나 발 링크 위치, 실제 접촉력, base roll·pitch, 미끄러짐, 모터 지연은 검사의 plant에 없습니다. 따라서 “step sequence PASS”를 “walking PASS”로 승격하면 안 됩니다.

Isaac 폐루프에서 1.16초 만에 드러난 것은 횡방향 안정성 부족이었습니다

첫 Isaac smoke에서 느린 전진 명령을 적용한 G1은 0.484m를 이동했지만 1.16초에 종료됐습니다. 8개 환경 모두 횡방향 속도 조건을 넘었고, 종료 직전 body-frame y 속도는 -1.048m/s였습니다. 목표 생성기는 앞뒤 stride를 만들고 있었지만 몸체가 옆으로 무너지는 속도를 잡지 못했습니다.

다음에는 아주 작은 명령에서 gait를 멈추는 deadband를 추가했습니다. 정지 명령의 y 속도는 -0.019m/s로 줄었습니다. 하지만 몸체 기울기는 0.781rad까지 커졌고 8개 환경 모두 1.24초에 tilt 조건으로 종료됐습니다. 전진 표류 하나를 줄였다고 standing 안정화가 해결된 것은 아니었습니다.

pitch feedback 부호를 바꾼 뒤 느린 보행은 1.28초, 정지는 1.32초로 조금 늘었습니다. 그러나 lateral·tilt 실패는 그대로였습니다. 이 순서는 실패 원인을 꽤 좁혀줍니다. 단일 부호 오류만의 문제가 아니라 stance 폭, hip-roll 목표, 실제 접촉 시점, base roll feedback과 actuator 응답이 함께 맞지 않는 상태입니다.

현재 파이프라인의 구현 상태를 한 표로 정리하면 이렇습니다

구성요소 구현 상태 확인된 증거 남은 경계
StateEstimator 경량 필터·접촉 임계값 구현 계산 단위 검사 contact-aided EKF와 장시간 drift 미검증
GaitScheduler stand·trot·walk 위상 구현 WALK DS 0.30, airborne 예정 구간 없음 예정 위상과 실제 touchdown 동기화 없음
FootTrajectory Bezier 위치·속도 구현 시작·끝·높이·속도 단위 검사 G1 전용 관절 목표 경로에 IK로 연결되지 않음
CoMPlanner LIPM·균등 GRF 목표 구현 step response와 높이 단위 검사 실제 CoP·하중 분배·force control 미연결
WbcQP SLSQP/heuristic q_des 구현 shape·joint-limit 검사 floating-base dynamics·contact constraint 미구현
G1 closed loop Isaac smoke 실행 실패 시각과 종료 원인 기록 안정 보행 acceptance 미통과

이 표에서 가장 중요한 칸은 “구현 상태”보다 “남은 경계”입니다. 파일이 존재하고 단위 검사가 통과했다고 상위 시스템의 목적까지 달성된 것은 아닙니다. WBC와 RL의 역할 및 승인 경계에서도 같은 이유로 현재 WBC walking을 router의 locomotion fallback으로 승격하지 않았습니다.

다음 검사는 관절 목표가 아니라 단계 사이의 오차를 기록해야 합니다

이제 필요한 것은 곡선 모양을 더 보기 좋게 만드는 일이 아닙니다. 계획값과 실현값을 같은 시간축에서 비교해 어느 단계에서 오차가 커지는지 찾는 것입니다. 다음 Isaac 실험에는 아래 값을 한 행에 함께 기록할 예정입니다.

  • 예정 contact와 측정 contact, touchdown·liftoff 시간 차이
  • 목표 foot position과 실제 foot link position, 특히 좌우 간격과 swing 최고 높이
  • 목표 CoM 가속도·CoP 대푯값과 실제 CoM 위치·속도
  • 목표 q_des, 실제 q, PD 오차, clipping 전후 토크
  • base roll·pitch와 body-frame 횡속도, 종료 조건이 처음 임계값을 넘은 시점

첫 수정 후보는 stance width와 hip-roll feedback입니다. 다만 한 번에 gain, 발폭, contact threshold를 모두 바꾸지 않습니다. 실제 발 궤적을 기록해 task-space 목표가 필요한지 확인하고, 예정·측정 접촉의 어긋남을 먼저 수치화한 뒤 하나의 변경만 적용합니다. full WBC로 확장한다면 그다음에 Pinocchio 기반 동역학과 접촉력을 최적화 변수로 넣어야 합니다.

현재 결론은 단순합니다. 이 프로젝트에는 한 걸음을 구성하는 부품이 있습니다. scheduler는 시간을 접촉 모드로 나누고, 발과 CoM planner는 목표를 만들며, WBC-lite와 PD는 이를 관절 명령으로 바꿉니다. 그러나 G1 전용 실행 경로는 아직 발 궤적·CoM·접촉력 전체를 하나의 동역학 문제로 연결하지 않았고, Isaac의 폐루프 안정성도 통과하지 못했습니다. 다음 한 걸음은 더 그럴듯한 목표 배열이 아니라, 계획한 발과 실제 발 사이의 오차를 측정하는 데서 시작합니다.

함께 읽기: WBC와 강화학습의 역할 경계 · CoM·support polygon·ZMP로 읽는 균형 · 발 접촉·마찰·support phase · position·velocity·torque와 PD actuator · Physical AI 프로젝트 전체 파이프라인

기술 기준 시점: 2026-07-22. 구현과 수치는 origin/main@ee4d96esim/wbc, G1 WBC-lite 평가 코드와 저장된 평가·smoke 기록을 기준으로 작성했습니다. 이 글의 component PASS는 계산 및 인터페이스 검사이며, G1의 안정적인 WBC walking이나 실기 배포 승인을 뜻하지 않습니다.

댓글 달기

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

위로 스크롤