이번 단계 목표: NVIDIA 로봇 개발 스택이 뭔지 지도를 그리고, 이 시리즈 전용 conda 환경을 만들고, 첫 시뮬레이션(큐브 낙하)이 도는 것까지 확인합니다. 그리고 첫 실행에서 15분짜리 함정 하나를 만납니다.
왜 지도가 먼저인가
Genesis는 pip install genesis-world 한 줄이면 끝났습니다. NVIDIA 쪽은 시작하기 전에 이름부터 정리해야 합니다. Omniverse, Isaac Sim, Isaac Lab, PhysX, USD… 공식 문서를 처음 열면 이 이름들이 뒤섞여 나와서, 뭘 설치해야 하는지부터 헷갈립니다.

| 이름 | 정체 | Genesis에서의 대응물 |
|---|---|---|
| Omniverse (Kit) | NVIDIA의 3D 앱 플랫폼. Isaac Sim이 이 위에서 조립된 하나의 ‘앱’ | (없음 — Genesis는 단일 라이브러리) |
| Isaac Sim | 로봇 시뮬레이터 앱. 물리는 PhysX 5, 장면은 USD, 렌더는 RTX | gs.init() + Scene + 래스터 렌더러를 합친 것 |
| Isaac Lab | Isaac Sim 위의 로봇 학습 프레임워크. RL 환경·지형·센서·랜덤화 | 우리가 train/ 폴더에 손으로 짰던 것 전부 |
| PhysX 5 | 물리 엔진 (GPU 병렬) | Genesis의 자체 솔버(Taichi 기반) |
| USD | 장면 기술 포맷(Pixar 제작). 로봇·지형·조명이 전부 USD | URDF로 로봇만 기술했던 것의 확장판 |
| RSL-RL | PPO 학습 러너 | 동일 — 시즌 1과 같은 계열이라 비교가 공정합니다 |
마지막 줄이 중요합니다. 학습 알고리즘이 같은 물건이니, 나중에 결과가 다르면 그건 알고리즘 탓이 아니라 환경 설계 탓입니다.
개념 박스 — URDF vs USD URDF는 “로봇 한 대”의 관절·링크를 적는 XML입니다. USD는 로봇을 포함한 “장면 전체” (지형·조명·재질·물리 속성)를 담는 씬 그래프 포맷이에요. Isaac Sim도 URDF를 읽을 수 있지만(변환기 내장), 배포되는 로봇 에셋은 대부분 미리 튜닝된 USD입니다. 우리가 쓸 G1도 NVIDIA가 배포하는 USD를 받습니다 — 02편에서 이 차이가 실제 관절 구성 차이로 나타나는 걸 보게 됩니다.
환경 구축 — 전용 환경을 ‘복제’로 만들기
이 머신에는 이미 연구용으로 검증된 Isaac Lab 환경이 있습니다(Isaac Sim 5.1.0 + Isaac Lab v2.3.2, conda env isaaclab). 그걸 그대로 쓰면 이 시리즈에서 뭘 설치하다 연구 환경을 오염시킬 수 있고, 처음부터 새로 설치하면 수십 GB를 다시 받아야 합니다.
절충안은 conda 환경 복제입니다. 버전이 완전히 동일하게 고정되고, 다운로드가 없고, 이후 이 시리즈에서 pip로 뭘 깔아도 원본 환경은 무사합니다.
# 검증된 기존 환경을 통째로 복제 (~20GB 복사, 다운로드 없음)
conda create -n isaacsim_ai --clone isaaclab
conda activate isaacsim_ai
python -c "import torch, isaaclab; print(torch.__version__, isaaclab.__version__)"
# → 2.7.0+cu128 0.54.3
한 가지 알아둘 점. Isaac Lab 소스는 editable 설치라서 복제본도 같은 체크아웃을 바라봅니다. 우리는 그 소스를 고치지 않고(읽기 전용), 이 프로젝트의 코드는 전부 저장소의 scripts/에 둡니다.
| 항목 | 고정 버전 |
|---|---|
| Isaac Sim | 5.1.0 (pip 설치) |
| Isaac Lab | v2.3.2 (isaaclab 0.54.3) |
| Python / PyTorch | 3.11 / 2.7.0+cu128 |
| GPU / Driver | RTX 4060 Ti 16GB / 580.159.04 |
첫 시뮬레이션 — 큐브 떨어뜨리기
시즌 1과 똑같은 의식(儀式)입니다. 큐브 하나를 1m 높이에서 떨어뜨려 바닥에 안착하면 물리·GPU·설치가 전부 정상이라는 뜻이죠.
python scripts/00_smoke_test.py --headless
[시작] 큐브 높이: 0.998 m
[0.25s] 큐브 높이: 0.640 m
[0.50s] 큐브 높이: 0.100 m
[1.00s] 큐브 높이: 0.100 m
통과: 최종 높이 0.100 m (기대값 0.100 m)
0.2m 큐브가 1m에서 떨어져 정확히 중심 높이 0.100m에 안착했습니다. 코드 구조가 Genesis와 어떻게 다른지가 첫 배움입니다:
| 단계 | Genesis | Isaac Lab |
|---|---|---|
| 엔진 켜기 | gs.init(backend=gs.gpu) |
AppLauncher(args) — Isaac Sim 앱 전체가 뜸 |
| 씬 만들기 | gs.Scene() |
SimulationContext(SimulationCfg(...)) |
| 물체 추가 | scene.add_entity(gs.morphs.Box(...)) |
RigidObjectCfg(spawn=CuboidCfg(...)) → RigidObject(cfg) |
| 굴리기 | scene.step() |
sim.step() + obj.update(dt) |
막힌 점 — import 순서 Isaac Sim 관련 모듈은
AppLauncher로 앱을 켠 뒤에만 import할 수 있습니다. 습관대로 파일 맨 위에 모아 두면 앱이 죽어요. 그래서 Isaac Lab의 모든 스크립트는 “argparse → AppLauncher → 그 다음 import”라는 어색한 구조를 갖습니다.
시간 감각도 실측해 뒀습니다. 이 무게가 Isaac Sim의 입장료입니다.
| 항목 | 시간 |
|---|---|
| 맨 첫 기동 (확장 캐시·셰이더 빌드) | 10분 이상 — 로그 없이는 멈춘 걸로 착각할 수준 |
| 이후 기동 (headless) | 약 7초 |
| 물리 120스텝 (큐브 1개) | 1초 미만 |
Genesis는 첫 실행부터 수 초였습니다. 대신 얻는 것(지형·센서·랜덤화·산업 표준)은 이후 편에서 하나씩 확인합니다.
15분짜리 함정 — 다 끝났는데 안 끝난다
첫 실행에서 스크립트가 15분 넘게 “돌고” 있길래 캐시 빌드려니 했습니다. 그런데 로그를 열어 보니 시뮬레이션은 7초 만에 전부 성공했고, simulation_app.close()가 그 뒤로 15분째 CPU만 태우며 멈춰 있었습니다.
프로세스 안에서 스택을 떠 보니(faulthandler) 범인이 나왔습니다. Isaac Lab이 타임라인 STOP 이벤트에 걸어 둔 콜백이었어요. 앱을 닫으면 타임라인이 멈추고, 콜백은 while not is_playing(): render() — “다시 재생될 때까지 렌더”라는 무한 루프에 들어갑니다. GUI에서 시뮬 종료 후 창을 유지해 주는 기능인데, headless 스크립트에서는 탈출구가 없는 겁니다.
해법은 close() 전에 그 구독을 해제하는 것(sim._app_control_on_stop_handle.unsubscribe()). 이후 전체 실행이 8초에 깔끔하게 끝납니다. 이 처리는 scripts/common.py의 shutdown()에 담아 모든 스크립트가 씁니다.
덤으로 얻은 교훈 둘:
- 오래 걸리면 진행 로그부터 열어 볼 것. “느린 것”과 “멈춘 것”은 다릅니다.
- 결과는
close()이전에 출력하고python -u(무버퍼)로 실행할 것. 버퍼에 갇힌 출력은 프로세스를 죽이는 순간 같이 사라집니다. 한 번 그렇게 결과를 잃었습니다.
개념 박스 — headless와 첫 기동
--headless는 GUI 없이 물리만 돌리는 모드입니다. 학습은 전부 headless로 합니다. Isaac Sim은 첫 기동 때 확장(extension) 캐시와 셰이더를 빌드하느라 한참 걸리고, 두 번째부터 빨라집니다. “설치가 잘못됐나?” 싶을 만큼 느린 첫 실행은 정상이에요.
다음 단계
큐브는 떨어졌습니다. 코드를 더 쓰기 전에, Isaac Lab이 세상을 어떻게 조직하는지 뼈대부터 잡습니다 → 01. Isaac Lab 기본 구조