URDF와 USD는 무엇이 다른가: 같은 G1이 시뮬레이터에서 다르게 움직이는 이유

G1에 아무 행동도 주지 않은 zero-policy 검사에서, 양쪽 발목 토크는 시작 후 0.02초 만에 한계에 도달했습니다. 몸의 무게중심이나 ZMP가 지지 영역을 벗어나기 전이었습니다. 첫 종료는 약 2.684초 뒤에 일어났지만, 실제 첫 이상 신호는 거의 시작과 동시에 나온 셈이었습니다.

이 상황에서 PPO의 학습률이나 reward weight를 바꾸는 것은 순서가 아니었습니다. 정책이 0을 출력해도 같은 현상이 생겼기 때문입니다. 제가 먼저 확인해야 했던 것은 화면에 보이는 G1의 생김새가 아니라, URDF의 링크·관절·충돌·관성 정보가 USD articulation으로 어떻게 옮겨졌고, Isaac Lab의 actuator 설정이 그 위에 어떤 값을 적용했는가였습니다.

URDF와 USD의 차이는 파일 확장자의 차이로 끝나지 않습니다. URDF는 로봇 구조를 전달하는 원본에 가깝고, USD는 시뮬레이터가 실제 장면 안에서 조합하고 실행하는 자산입니다. 그리고 최종 동작은 USD 하나가 아니라 초기 자세, actuator gain과 limit, action scale까지 포함한 실행 계약에서 결정됩니다.

URDF 원본, Isaac Sim importer, USD articulation, Isaac Lab actuator 설정, runtime trace로 이어지는 G1 자산 검증 흐름과 v87부터 v89까지의 교차 실험 결과를 정리한 다이어그램
같은 형상의 G1이라도 URDF 변환 옵션, USD 물리 스키마, Isaac Lab actuator override가 달라지면 다른 동역학을 만듭니다. 그래서 검증은 화면이 아니라 runtime trace까지 이어져야 합니다.

URDF는 로봇의 구조를 전달하지만 실행 결과까지 보장하지 않습니다

URDF는 링크와 관절로 이루어진 트리 구조를 기술합니다. 각 링크에는 화면에 보여 줄 visual, 접촉 계산에 사용할 collision, 질량과 관성 모멘트를 담는 inertial을 둘 수 있습니다. 관절에는 parent와 child, 회전축, limit, dynamics 같은 정보가 들어갑니다. ROS 2의 URDF 물리·충돌 설명도 visual과 collision을 분리해서 다루고, 시뮬레이션에 쓰는 링크에는 inertial 정보가 필요하다고 설명합니다.

여기서 중요한 점은 visual mesh가 정상이라고 collision도 정상인 것은 아니라는 사실입니다. 표면이 매끈한 발이 화면에 보이더라도 실제 접촉 형상이 없거나 너무 작으면 물리 엔진 입장에서는 바닥을 지탱할 발이 없습니다. 반대로 collision을 지나치게 복잡한 삼각형 mesh로 만들면 접촉 계산이 불안정하거나 느려질 수 있습니다.

URDF는 읽기 쉽고 여러 로봇 도구가 공유하기 좋은 구조 기술 형식이지만, Isaac Sim의 장면 그래프와 composition, physics schema, instancing을 그대로 표현하는 런타임 패키지는 아닙니다. 그래서 현재 프로젝트도 g1_29dof.urdf를 직접 학습 환경에 넣지 않고, importer를 거쳐 USD로 만든 뒤 불러왔습니다.

USD는 변환된 3D 파일이 아니라 시뮬레이터가 실행하는 장면입니다

OpenUSD Physics schema에는 rigid body, collision, joint, articulation root 같은 물리 의미가 정의돼 있습니다. Isaac Sim은 이 구조를 사용해 로봇을 장면의 prim hierarchy로 배치하고 PhysX가 계산할 articulation으로 만듭니다. 즉 USD 안에는 mesh 경로만 있는 것이 아니라, 어떤 prim이 rigid body인지, 어느 joint가 링크를 잇는지, 어느 형상이 collider인지, 로봇 전체의 articulation root가 어디인지가 함께 있어야 합니다.

USD의 장점은 로봇만 담는 데 있지 않습니다. 로봇, 바닥, 센서, 조명, 재질을 한 stage에서 조합하고, 다른 파일을 reference하거나 layer로 덮어쓸 수 있습니다. 대신 “URDF를 USD로 변환했다”는 한 문장만으로는 실행 자산의 상태를 알 수 없습니다. importer가 무엇을 합쳤고 어떤 물리 속성을 만들었는지까지 확인해야 합니다.

Isaac Sim의 URDF importer도 URDF를 파싱한 뒤 import configuration에 따라 USD stage를 만듭니다. 이 설정이 새로운 로봇의 첫 번째 설계 결정입니다.

제 변환 스크립트가 결정한 네 가지

현재 저장소의 sim/scripts/convert_urdf_to_usd.py는 Isaac Sim 5.1 standalone 환경에서 G1 URDF를 USD로 변환합니다. 당시 고정한 옵션은 다음과 같습니다.

  • fix_base=False: G1의 base를 월드에 고정하지 않고 떠 있는 로봇으로 만듭니다.
  • merge_fixed=True: fixed joint로 이어진 링크를 importer가 병합하도록 합니다.
  • self_collision=False: 변환 시 self-collision을 기본 활성화하지 않습니다.
  • distance_scale=1.0, density=0.0: URDF의 metre 단위와 명시된 inertia를 사용합니다.

변환 뒤에는 wrapper stage에 defaultPrim이 비어 있어 robot root를 다시 지정했습니다. defaultPrim은 보행을 잘하게 만드는 숫자는 아니지만, USD를 다른 stage에 reference할 때 어느 prim을 기본 진입점으로 사용할지 결정합니다. 이 한 단계가 빠지면 파일은 열리더라도 composition 방식에 따라 의도한 robot root를 잡지 못할 수 있습니다.

merge_fixed도 단순한 용량 최적화 옵션이 아닙니다. 링크 구조가 바뀌면 prim path, rigid body 수, 질량이 귀속되는 위치가 달라질 수 있습니다. 변환 성공 메시지 뒤에 joint count와 name만 확인해서는 부족하고, collision·mass·inertia·articulation root를 runtime stage에서 다시 읽어야 하는 이유입니다.

발 모양은 있었지만 실제 collider는 없었습니다

G1 초기 진단에서 가장 직접적인 실패는 발 collision이었습니다. source URDF에는 양쪽 ankle_roll_link 아래에 반지름 0.005m의 작은 sole contact sphere가 네 개씩 정의돼 있었습니다. 그러나 변환된 runtime USD를 검색했을 때 관련 path는 16개가 나왔지만 collision_like_prims=0이었습니다. ankle collision이라는 이름의 Xform은 존재했지만, 실제 충돌을 담당할 prim은 찾지 못했습니다.

그래서 runtime에 sole box collider를 주입하고 USD에도 직접 collision prim을 추가했습니다. 화면과 stage 구조에서는 발바닥이 생겼습니다. 하지만 5초 zero-policy 검사 결과는 거의 바뀌지 않았습니다. 첫 종료 평균은 0.4975초였고, 환경당 reset은 평균 10.25회였습니다. 수정 전 약 0.495초와 사실상 같은 수준이었습니다.

여기서 두 가지를 분리했습니다. 첫째, 변환 과정에서 collision이 유실된 문제는 실제였습니다. 둘째, collision prim을 보이게 만든 것만으로 지지 실패의 원인이 해결됐다고 말할 수는 없었습니다. collider가 PhysX contact set에 올바르게 포함됐는지, 초기 발 높이가 바닥과 맞는지, reset 순간 penetration이 없는지, 관성과 actuator가 같은 자세를 지탱할 수 있는지는 별도의 검사였습니다.

terrain을 유한 patch에서 infinite plane으로 바꿨을 때 첫 reset이 1.90초까지 늦어진 기록도 있었습니다. 이것은 바닥 구성도 결과에 영향을 줬다는 증거지만, G1 asset이 정상이라는 증명은 아니었습니다. 한 번의 생존 시간은 여러 오류가 섞인 결과이므로 각 입력을 따로 고정해야 했습니다.

USD 위에는 Isaac Lab의 actuator 설정이 다시 적용됩니다

USD가 완성돼도 최종 관절 동작은 아직 정해지지 않습니다. Isaac Lab의 articulation configuration 설명처럼 ArticulationCfg는 USD 경로와 초기 상태뿐 아니라 actuator group도 지정합니다. actuator config에서 stiffness, damping, effort limit, armature를 override하면 USD에 기록된 값 대신 적용된 설정이 runtime 동작을 바꿉니다.

제 기존 G1_CFG는 다리 group에 stiffness 1300, damping 60, simulator effort limit 900을 사용했습니다. 반면 현재의 official G1 설정은 관절 역할에 따라 hip·knee stiffness를 대략 100–150, damping을 2–4, effort limit을 88 또는 139로 나누고 armature 0.01을 둡니다. 숫자가 크게 다르다고 어느 쪽이 자동으로 옳은 것은 아닙니다. 핵심은 한 자산을 위해 정한 actuator 설정을 다른 USD에 그대로 붙이면 같은 제어기가 되지 않는다는 점입니다.

position target을 쓰는 경우 stiffness와 damping은 목표 각도 오차와 관절 속도를 토크로 바꾸는 PD 관계에 관여합니다. effort limit은 계산된 토크가 실제 simulator에 들어가기 전의 한계를 정합니다. armature는 관절의 유효 관성을 바꾸어 수치 안정성과 응답에 영향을 줄 수 있습니다. 따라서 q_target이 같아도 적용되는 토크와 다음 관측은 USD–actuator 조합마다 달라집니다.

v78–v87에서 세 번 멈춰 선 이유

초기에는 stiffness와 damping, effort limit을 바꾸며 zero-policy standing 시간을 늘리려 했습니다. v82의 첫 낙상은 2.684초, v85는 2.58초, v87은 다시 2.684초였습니다. 세 번의 조정 뒤에도 약 2.6–2.7초에서 정체됐습니다. 이 시점에 더 많은 parameter scan을 중단했습니다.

v87 trace를 시간순으로 보면 이유가 선명했습니다. t=0에서 발목 토크 비율은 좌우 0.635와 0.682였고, t=0.02에는 0.994와 1.000으로 올라갔습니다. 반면 ZMP margin이 음수가 된 시점은 0.78초 부근이었습니다. 2.60초에는 pitch가 +0.909 rad까지 커진 상태에서 토크가 포화돼 있었습니다.

넘어지기 직전의 자세만 보면 “균형을 잃어서 토크가 커졌다”고 해석하기 쉽습니다. 하지만 trace의 인과 순서는 반대였습니다. 발목이 먼저 포화됐고, 그 뒤 지지 영역과 자세가 무너졌습니다. v83에서는 stiffness와 damping만 바꾸고 effort limit과 armature는 그대로 두었으므로, actuator 가설도 절반만 검사한 상태였습니다. 이 기록이 reward와 PPO를 계속 조정하지 않고 asset–actuator 경계를 분리하게 만든 근거입니다.

공식 USD와 actuator를 교차한 결과

v88에서는 official Unitree G1 29DOF rev 1.0 USD와 그 자산에 맞춘 actuator 설정을 함께 사용했습니다. reward와 PPO는 건드리지 않고 16개 환경에서 5초 zero-policy 진단만 실행했습니다. 기본 자세 조건에서 첫 종료는 0.92초였고, 16개 모두 height termination이었습니다. 하지만 즉시 토크 포화는 0/16으로 사라졌습니다.

생존 시간만 보면 2.684초에서 0.92초로 나빠졌습니다. 그러나 실패 signature는 “시작 직후 고토크 포화”에서 “포화 없이 앞으로 기울며 height로 종료”로 바뀌었습니다. 공식 조합이 걷기에 성공했다는 뜻은 아니지만, 기존 조합과 다른 물리·구동 계약이라는 것은 확인할 수 있었습니다.

v89에서는 USD와 actuator를 교차해 어느 한쪽이 원인인지 확인했습니다.

USD / actuator 첫 종료 토크 포화 해석
official / official 0.92초 0/16 즉시 포화는 사라졌지만 height로 종료
current / official 0.76초 16/16, 평균 0.04초 official actuator가 자체 변환 USD의 drop-in 대체가 아님
official / current 0.94초 16/16, 평균 0.901초 기존 gain도 official USD와 그대로 호환되지 않음

자체 USD에 official actuator를 붙인 조합은 가장 빨리 무너졌고 즉시 포화가 되살아났습니다. official USD에 기존 actuator를 붙인 조합도 종료 직전 전 환경에서 포화됐습니다. 어느 파일 하나를 “좋은 USD”, 어느 gain 하나를 “좋은 actuator”로 뽑아 교체할 수 없었습니다. asset과 actuator는 함께 검증해야 하는 결합된 계약이었습니다.

0.92초 낙상을 자산 실패로 판정하면 안 됐던 이유

v88 당시에는 4초 동안 zero action으로 서 있어야 자산이 정상이라는 gate를 사용했습니다. 이후 이 기준 자체를 다시 계산했습니다. official 설정에서 양쪽 발목의 총 stiffness는 약 80 Nm/rad인데, G1 질량 35.115kg과 자세에서 단순화한 중력 복원 요구량 mgh는 약 248 Nm/rad였습니다. 능동적으로 균형을 잡는 정책 없이 passive PD만으로 4초를 버티라는 요구는 현실적인 asset sanity check가 아니었습니다.

v90에서는 official nominal pose를 environment의 robot.data.default_joint_pos와 동기화하고, action scale을 0.25로 고정했습니다. 그 결과 zero-policy 첫 종료는 1.14초였고 즉시 포화와 NaN은 없었습니다. 학습용 termination을 적용하면 1.28초에 16개 모두 tilt로 종료됐습니다. 이것을 정지 성공으로 부르지는 않았지만, 초기 침투나 수치 폭발 없이 자연스럽게 넘어지는 범위는 확인했습니다.

현재 asset sanity gate는 “정책 없이 오래 선다”가 아닙니다. NaN이 없는지, 첫 step부터 effort limit에 붙지 않는지, 바닥을 관통하지 않는지, nominal pose에서 예상 가능한 방향과 시간으로 넘어지는지를 봅니다. 균형 회복은 그 다음 PPO가 학습해야 할 문제입니다.

새 로봇 자산을 PPO 전에 검사하는 순서

이 시행착오 뒤에는 URDF import부터 PPO까지 한 번에 열지 않습니다. 아래 순서에서 앞 단계가 실패하면 뒤 단계의 reward와 optimizer는 건드리지 않습니다.

  1. 구조 검사: link·joint 수, 이름, parent–child, 회전축, runtime joint order를 source와 비교합니다.
  2. 물리 형상 검사: visual과 collision을 따로 표시하고, sole collider와 self-collision, articulation root가 실제 stage에 존재하는지 확인합니다.
  3. 질량 검사: 각 링크 mass·inertia·CoM, 전체 질량, 단위를 읽어 URDF 원본과 비교합니다.
  4. 적용값 검사: USD에 저장된 drive 값이 아니라 runtime의 applied stiffness·damping·effort limit·armature를 출력합니다.
  5. reset 검사: nominal joint pose, root height, sole height, floor 높이와 초기 penetration을 함께 기록합니다.
  6. zero-policy trace: height나 생존 시간만 보지 않고 torque ratio, contact, CoM/ZMP, termination cause의 최초 이상 시점을 찾습니다.
  7. 작은 수동 명령: 제한된 joint target으로 방향, action index, tracking authority와 saturation을 확인합니다.
  8. 그다음 PPO: 환경 계약이 통과한 뒤에야 reward, observation과 optimizer를 학습 변수로 엽니다.

이 순서는 asset이 물리적으로 정확하다는 최종 인증 절차는 아닙니다. 다만 보이지 않는 collider 문제를 reward로 가리거나, action index 오류를 PPO 탐색 부족으로 오해하는 일을 줄여 줍니다. Sim·Lab·RSL-RL의 더 넓은 역할 구분은 Isaac Sim과 Isaac Lab의 경계에서, 실제와 맞춰야 할 오차 항목은 G1 sim-to-real 검증 글에서 따로 다뤘습니다.

현재 채택한 경계와 아직 말할 수 없는 것

상태 현재 근거 확대해서 말하면 안 되는 것
Implemented official G1 rev 1.0 USD, official 계열 actuator config, nominal pose 동기화 실물 G1과 동역학이 일치한다는 뜻은 아님
Executed v88–v90 zero-policy와 USD/actuator 교차 진단 보행 정책이 승인됐다는 뜻은 아님
Observed official pair에서 즉시 포화가 사라지고 실패 signature가 바뀜 0.92초보다 2.684초인 기존 asset이 더 낫다는 뜻은 아님
Accepted 현재 승인된 G1 보행 checkpoint 없음 asset sanity 통과를 walking success로 부를 수 없음
Deployed 실물 G1 배포 기록 없음 sim-to-real 성능과 안전성을 주장할 수 없음

origin/main@ee4d96e 기준으로 학습 경로는 old converted asset 대신 official Unitree USD와 현재 actuator 계약을 사용합니다. 그러나 이것은 시작점을 고정한 상태입니다. 현재 프로젝트에는 승인된 G1 보행 checkpoint가 없으며, 실물 G1의 motor·sensor trace와 비교한 system identification도 아직 없습니다.

URDF에서 USD로 넘어갈 때 보존해야 하는 것은 로봇의 겉모습이 아닙니다. 링크와 관절의 의미, 접촉과 관성, actuator가 만드는 입력–응답 관계입니다. 같은 G1이 다르게 움직인다면 먼저 “어느 파일이 틀렸나”라고 묻기보다, URDF 원본부터 runtime trace까지 어느 경계에서 계약이 바뀌었는가를 추적해야 합니다. 그 경계를 확인한 뒤에야 PPO의 실패를 학습 문제로 다룰 수 있습니다.

함께 읽기: Isaac Sim·Isaac Lab 로컬 실행 환경 · Isaac Sim과 Isaac Lab의 역할 차이 · G1 강화학습 환경의 관측·행동·보상 · 현재 G1 PPO 학습과 중단 기준 · Sim-to-real 검증 계약

공식 문서: ROS 2 URDF physical and collision properties · Isaac Sim URDF importer · OpenUSD Physics schema · Isaac Lab articulation configuration

댓글 달기

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

위로 스크롤