Search
🔵

Isaac Sim 개발 환경 구축하기

배경

이 문서는 클라우드 등의 리소스를 레버리징하여 개인 레벨에서 물류창고용 모바일 매니퓰레이터를 개발해 보기 위한 여정의 일부입니다. 그 중에서도, 각종 머신러닝 알고리즘을 빠르게 통합하고 검증하기 위한 Isaac Sim 환경을 세팅하는 과정이 담겨 있습니다.

개발 서버 선택하기

Isaac Sim은 RTX 기술을 기반으로 광선 추적(Ray Tracing) 렌더링을 수행합니다. 또한, 클라우드 서버에는 모니터가 연결되어 있지 않으므로, 렌더링된 화면을 사용자의 PC로 전송하기 위해서는 실시간 비디오 압축이 필요합니다. 이때 필수적인 기술이 NVENC(NVIDIA Encoder)라는 하드웨어 가속 비디오 인코딩 엔진입니다.
참고로 NVIDIA A100 및 H100 GPU는 대규모 언어 모델(LLM) 학습에 특화된 연산 가속기입니다. 이들은 그래픽 출력 단자가 없을 뿐만 아니라, Isaac Sim의 원격 스트리밍에 필수적인 NVENC 엔진을 탑재하고 있지 않습니다(ref1).
Issac 5.1.0 문서에서 공식적으로 추천하는 인스턴스는 GCP의 G2 머신 시리즈를 통해 제공되는 L4이므로 저는 이것을 사용하도록 하겠습니다. 나머지 스펙은 Ideal과 Good 스펙(SSD > 1024GB, RAM > 48GB, CPU x86 > 8core)으로 맞춰 줍니다.
최대한 빠르게 통신하기 위해 리전은 서울 리전(asia-northeast3)으로 하고, 머신러닝 학습용 인스턴스가 아니므로 GPU를 언제든지 뺐겨도 상관이 없으므로 스팟성 인스턴스로 세팅하여 비용을 절약하도록 합니다. 인스턴스를 뺐겼을 때 아예 데이터가 삭제되지 않도록 설정하는 부분만 주의합니다(On VM termination: Stop). 나머지는 Issac 5.1.0 문서에서 명시하는 OS(Ubuntu 22.04 LTS)를 사용합니다. 방화벽은 이번 단계에서는 HTTP, HTTPS만을 열어 두고 나머지는 조금 이따가 마무리하도록 합시다. 어차피 개발 시 후술할 Tailscale을 사용할 것이기 때문입니다.
sudo adduser janghoo sudo usermod -aG sudo janghoo
Bash
복사
제가 사용할 리눅스 계정을 새로 만들었습니다.

개발 서버에 연결하기

하드웨어 사양을 확정한 후에는 실제 클라우드 환경을 구축해야 합니다. 이 과정에서 가장 중요한 것은 외부에서 GUI 없이 돌아가는 시뮬레이터에 접속할 수 있도록 네트워크 길을 터주는 보안 설정과, 시뮬레이션 성능을 극대화하기 위한 드라이버 설정입니다.
'헤드리스(Headless)' 모드로 구동되는 Isaac Sim은 물리적인 디스플레이 장치가 없으므로, WebRTC(Web Real-Time Communication) 프로토콜을 통해 영상과 제어 신호를 주고받습니다. 이 프로토콜이 원활하게 작동하기 위해서는 특정 포트에 대한 방화벽 해제가 필수적입니다. 가령 Isaac Sim 문서에는 49100, 47998 포트 및 8211(Isaac Sim의 HTTP 서버 포트), 22(SSH) 등을 열라고 되어 있지만, 이는 인터넷을 통해 VM에 직접 접속할 때의 이야기입니다. Tailscale을 사용한다면 GCP 콘솔에서 복잡한 포트 규칙을 일일이 만들 필요가 없습니다. 포트 하나와 로그인 한 번으로 내 로컬 PC와 GCP VM 사이에 가상의 전용 랜선을 깔아 줍니다.
우리가 열어야 할 포트는 UDP 41641 입니다. GCP 방화벽에서 UDP 41641 포트 하나만 열어주면, 내 PC와 GCP VM이 중간 단계 없이 1:1로 직접 데이터를 주고받습니다. VM에 연결된 네트워크의 방화벽 설정에서 Ingress UDP 41641을 열어 주세요(GCP의 기본 방화벽 설정은 나가는 것(Egress)은 다 허용, 들어오는 것(Ingress)은 다 차단이 원칙). 물론 Tailscale은 41641 포트를 통해 직접 연결을 맺지 않아도 어떻게든 연결을 해냅니다. 하지만 직접 연결이 안 되면 Tailscale에서 운영하는 중간 서버를 거쳐서 데이터가 이동합니다.
Target tags는 식별자입니다.
VM 세팅에서 앞서 지정했던 Target tags의 태그를 등록해 줍니다.
내 PC에서 스트리밍 클라이언트를 켜고, VM의 Tailscale IP 주소와 49100번 포트로 접속을 시도합니다. 내 PC에 깔린 Tailscale이 이 49100번 데이터를 가로챕니다. 그리고 이를 암호화하여 'UDP 41641번'이라는 이름의 큰 상자에 집어넣습니다. 이 '41641번 상자'는 인터넷을 타고 GCP 서버로 날아갑니다. GCP의 VPC 방화벽은 외부에서 들어오는 모든 문을 잠갔지만, UDP 41641번 문 하나만 열어둔 상태입니다. 방화벽은 이 상자가 41641번임을 확인하고 통과시켜 줍니다. (이때 방화벽은 상자 안에 49100번 데이터가 들었는지 전혀 모릅니다.) VM 안에 있는 Tailscale이 방화벽을 통과해 들어온 '41641번 상자'를 받아서 암호를 풀고 내용물을 꺼냅니다. 꺼내 보니 "49100번 포트로 가야 할 데이터"입니다. Tailscale은 이를 VM 내부에서 실행 중인 Isaac Sim(서버)에 전달합니다. 이제 Isaac Sim 서버가 내 PC로 고화질 영상 데이터(UDP 47998 등)를 보내야 합니다. VM의 Tailscale이 이 영상 데이터를 다시 'UDP 41641번 상자'에 넣어 내 PC의 공인 IP로 쏩니다. 그럼 내 PC의 Tailscale이 이 상자를 받아 알맹이(영상 데이터)만 쏙 빼서 내 스트리밍 클라이언트 프로그램에 전달합니다.
보통 개발 시에 필요한 SSH도 마찬가지입니다. 내가 SSH 클라이언트로 서버의 Tailscale IP(예: 100.1.2.3)의 22번 포트에 접속을 시도하면, 내 PC의 Tailscale 소프트웨어가 이 요청을 가로채서 열린 포트로 보냅니다. GCP는 그 패킷을 받아 해체해서 실제로 SSH 연결인 것처럼 22번 포트로 넣어 줍니다.
로컬 Tailscale 셋업과 tailnet 완료되었음을 전제로, 아래 명령어로 VM에 Tailscale을 설치하고 실행합니다.
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up --ssh # --ssh: Tailscale 이 ssh 연결 시 비밀번호를 자동으로 관리합니다.
Bash
복사

개발 환경 준비하기

Issac Sim 문서에서는 GCP용 NVIDIA driver 설치 스크립트를 사용하라고 권고합니다.
curl https://raw.githubusercontent.com/GoogleCloudPlatform/compute-gpu-installation/main/linux/install_gpu_driver.py --output install_gpu_driver.py sudo python3 install_gpu_driver.py
Bash
복사
Isaac Sim 환경 Python 환경 기반 설치(권장)
Docker 기반 설치를 권장하지 않는 이유
uv 설치: 시스템 파이썬 패키지와의 충돌을 방지하고 python 3.11 개발 환경 및 Isaac Sim 엔진의 독립성을 보장하기 위해 uv를 사용합니다.
curl -LsSf https://astral.sh/uv/install.sh | sh
Bash
복사
uv 가상환경 준비
uv init my-first-isaac-project --python 3.11 && cd my-first-isaac-project uv venv --python 3.11 && source .venv/bin/activate
Bash
복사
uv add "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.com --no-cache uv run -m isaacsim --generate-vscode-settings
Bash
복사
--no-cache를 사용하는 이유와 트레이드오프
일반적인 Python 패키지는 설치 후 파일을 수정하지 않지만, Isaac Sim은 확장 시스템이라는 독특한 구조 때문에, "소스코드는 아니지만 소스코드처럼 작동하는 바이너리와 설정값"들을 extscache에 쌓게 됩니다(ref10). Isaac Sim은 extscache 폴더 안에 바이너리(C++ 파일 등)를 직접 쓰거나 수정합니다.
uv는 패키지가 설치된 후에는 변하지 않는다는 점을 이용하여 하드 링크를 사용해 여러 환경에서 공유합니다(pip는 패키지를 설치할 때 전역 캐시에서 파일을 복사해서 가져옵니다)(ref11). 이때 uv로 연결된 파일이 수정되면, 전역 캐시의 원본까지 변해버릴 수 있고, 이것이 다른 가상환경에서 경로가 꼬이는 원인이 됩니다.
원래 Omniverse는 python package가 아닌 전용 런처를 통해 설치되던 프로그램이었습니다. 이를 최근에 pip install이 가능하도록 이식하는 과정에서, 기존의 "설치 폴더 내 캐시 관리" 로직이 현대적인 Python 패키지 배포 표준과 충돌하게 된 것입니다.
--no-cache 옵션은 문제를 해결하는 가장 확실한 방법이지만, 몇 가지 기회비용이 발생합니다. uv의 가장 큰 장점 중 하나는 여러 가상환경이 있어도 실제 파일은 전역 캐시에 딱 하나만 두고 링크해서 쓰는 것입니다. Isaac Sim은 패키지 크기만 수 GB에서 수십 GB에 달합니다. 캐시를 사용하면 10초 남짓에 설치가 완료되지만, 캐시를 사용하지 않으면 수십분간 매번 이 거대한 파일을 다시 다운로드하고 압축을 풀어야 합니다.
저는 아무리 그래도 Isaac Sim이 라이브러리 소스코드를 건드리기보다는, 본인의 캐시 디렉토리만 건드릴 것이라고 생각합니다. 그래서 저는 다음과 같이 rm -rf로 Isaac Sim은 확장 시스템디렉토리를 지우는 방식을 권장합니다. 해당 가상환경에서 이전 가상환경의 영향이 깔끔하게 사라지므로 문제가 해결됩니다.
uv add "isaacsim[all,extscache]==5.1.0" --extra-index-url https://pypi.nvidia.com uv run -m isaacsim --generate-vscode-settings rm -rf .venv/lib/python3.11/site-packages/isaacsim/extscache mkdir -p .venv/lib/python3.11/site-packages/isaacsim/extscache
Python
복사
다음과 같이 스크립트를 실행합니다. 하드웨어 스펙, GPU 드라이버, Vulkan 설정 등이 올바른지 확인합니다.
isaacsim isaacsim.exp.compatibility_check --no-window --/app/quitAfter=10
Bash
복사
문서에 따르면 인터넷 너머의 원격 연결인 경우 --/app/livestream/publicEndpointAddress 옵션을 명시해야 합니다(ref8). 이를 지정하지 않으면 스트리밍 클라이언트에서 화면이 보이지 않는 문제를 마주하실 수 있습니다. 100.65.116.2는 제가 Tailscale을 통해 받은 VM의 IP 주소입니다.
isaacsim isaacsim.exp.full.streaming \ --no-window \ --/app/livestream/publicEndpointAddress=100.65.116.2
Bash
복사
만약 10분 이상 기다렸는데 Isaac Sim Full Streaming App is loaded. 메시지가 나오지 않는다면, 누락된 시스템 의존성들이 있을 가능성이 높습니다. 사실 도커를 사용하면 이런 것을 관리할 일이 없지만, 지금은 pip를 사용하기 때문에 시스템 의존성을 직접 해결해 주어야 합니다. 오류 로그를 LLM에 넣고 얻은 의존성 패키지들입니다.
# 시스템 패키지 매니저 업데이트 sudo apt update # 1. X11 및 창 관리 라이브러리 (WebRTC 스트리밍 및 가상 디스플레이 필수) sudo apt install -y libxt6 libxrender1 libxrandr2 libxcursor1 libxinerama1 libxi6 # 2. OpenGL 및 렌더링 지원 (libGLU 에러 및 재질 로드 실패 해결) sudo apt install -y libglu1-mesa libosmesa6 libegl1 # 3. 시스템 유틸리티 및 오디오 (ROS 2 브릿지 및 시뮬레이션 엔진 안정화) sudo apt install -y libasound2 libpulse0 libnss3 libxcomposite1 libxss1 libfontconfig1
Bash
복사
Powered by LLM
pyproject.toml에 다음 내용을 추가합니다(ref9).
[tool.uv] environments = [ "sys_platform == 'linux'", ] override-dependencies = [ "pywin32==306; sys_platform == 'win32'" ]
Bash
복사
Isaac Sim 환경 Docker 기반 설치(권장하지 않음)
스트리밍 클라이언트 설치
macOS의 경우 여기서 macOS용 5.1.0 WebRTC 스트리밍 클라이언트를 다운로드받습니다.
Isaac Sim 문서를 보게 되면, 종종 "aarch64 지원 불가"라는 문구를 마주치게 되는데, 이는 macOS M 시리즈 클라이언트에 해당되는 이야기가 아닙니다.
Isaac Sim 스트리밍 클라이언트는 macOS M 시리즈 네이티브 앱을 지원합니다. 정 안되면 M1 Mac의 에뮬레이션 기능인 Rosetta 2를 사용하는 것도 방법입니다.
runheadless.sh을 실행했을 때 Isaac Sim Full Streaming App is loaded 로그를 확인했다면, 앞서 Tailscale을 통해 받은 VM의 주소 100.65.116.2를 입력해 연결합니다.
제 환경의 경우 QHD로 스트리밍하더라도 지연 시간이 8ms 수준밖에 되지 않아, 응답성이 상당히 좋은 원격 개발 환경이 구축되었음을 확인할 수 있습니다.
기타 개발 환경
VSCode
Tailscale SSH 덕분에 별다른 비밀번호 관리 없이 바로 연결 가능합니다.
zsh 설치
oh my zsh 설치 후 zsh를 기본 셸로 추가
fzf 설치 후 oh my zsh 플러그인으로 추가
dclient 설치

Isaac Sim 제어 방법에 대한 오해

python IsaacSim은 단순 리모컨이 아니다

이 문서(5.1.0)를 보면 python 스크립트를 이용하여 Isaac Sim을 제어하는 두 가지 방법을 소개하고 있습니다. 첫 번째는 Isaac Sim의 python 라이브러리 isaacsimpip install하여 사용하는 방식이고, 두 번째는 컨테이너를 이용해 설치한 경우 내부의 python 인터프리터를 실행하는 python.sh 래퍼 셸 스크립트 이용하는 방식입니다.
두 방식 모두 핵심은 SimulationApp 클래스에 있습니다. SimulationApp 클래스는 사용하여 NVIDIA Isaac Sim 애플리케이션의 생성부터 종료까지의 생명 주기를 관리합니다. 여기서 중요한 점은 Isaac Sim이 데이터베이스나 웹 서버처럼 백그라운드에 데몬(Daemon)으로 떠 있고 클라이언트가 접속하는 '클라이언트-서버' 방식이 아니라는 것입니다.
Isaac Sim의 Python 라이브러리는 단순한 API 클라이언트가 아니라, 내부적으로 시뮬레이션 엔진 본체(Omniverse Kit SDK 및 바이너리 라이브러리)를 직접 포함하고 있습니다. 따라서 스크립트를 실행하는 순간 해당 프로세스 자체가 시뮬레이터 엔진이 됩니다. python.sh 스크립트를 실행하는 방식도 마찬가지입니다. 그 어떤 방식이든 마치 스타크래프트 실행 파일을 두 번 누르는 것과 비슷합니다. 따라서 Isaac Sim 이미지를 사용 중이라면 무거운 엔진의 재설치가 필요하지 않은 python.sh를 이용해야 합니다.

n개의 python 스크립트가 하나의 엔진에 붙는 구조가 아니다

이러한 Isaac Sim 엔진을 여러 개의 독립적인 독립 실행형 Python 프로세스가 직접 공유하도록 설계되어 있지는 않습니다. Isaac Sim 라이브러리의 SimulationApp은 엔진의 심장(메인 루프)을 직접 제어합니다. 시뮬레이션의 '시간'을 누가 흐르게 할지, '렌더링'을 언제 할지는 단 하나의 메인 루프에서 결정되어야 합니다. n개의 스크립트가 제각각 world.step()을 호출한다면 시뮬레이션의 시간 축은 엉망이 됩니다.
이미 컨테이너 환경에서 실행 중인 Isaac Sim을 엔진으로 두고 조종만 하고 싶다면, ROS(omni-isaac-ros2-bridge) 등을 활용하거나, 별도의 통신 레이어를 직접 구현해야 합니다. 또는 앞서 언급했던 익스텐션으로 등록하는 방법도 있습니다.

Isaac Sim과 Omniverse Kit에서 바라본 USD

NVIDIA의 Omniverse Kit에서 모든 것은 확장입니다(ref3). 렌더링, 물리 엔진, UI 조차도 Kit의 내장 기능이 아니라, 필요할 때 로드하는 확장 기능(Extension)입니다.
갑자기 Omniverse Kit는 어디서 튀어나온건가 싶겠지만, Isaac Sim은 사실 Omniverse Kit Extension 집합에 이름을 붙여 둔 것이기 때문에 이해하고 넘어가야 합니다. 레고의 비유를 들면, ‘Omniverse Kit Extension’은 "톱니바퀴 팩", "투명 브릭 팩", "모터 팩"처럼 특정 기능만 모아둔 작은 봉지입니다. 한편 Issac Sim은 "우주 정거장"이라는 테마를 위해 수천 개의 팩들을(Extension)들을 한 박스에 담아 출시한 거대한 세트 패키지인 셈입니다. 그래서 개발자는 Isaac Sim에 필요한 것이 있다면 플러그인 단위로 추가하거나 수정할 수 있습니다.
예를 들어, 물리 연산을 수행하는 C++ 라이브러리로 PhysX라는 것이 있다고 해 봅시다. 이를 Omniverse Kit에서 사용할 수 있도록 감싸둔 래퍼(Wrapper)가 omni.physx이라는 확장입니다. 이 확장은 USD라는 3D 데이터를 읽어서 물리 엔진에 던져주고, 계산된 결과(위치 이동, 충돌 등)를 다시 USD 데이터로 기록하는 역할을 합니다. 기존의 도구들을 다시 만드는 것이 아니라, 하나의 목적으로 잘 엮어 사용할 수 있는 플랫폼을 만들고자 했던 설계 의도가 잘 드러납니다(ref4).
Isaac Sim의 모든 모델은 USD(Universal Scene Description)로 정의됩니다. USD(Universal Scene Description)는 우리에게 잘 알려진 애니메이션 스튜디오 Pixar가 개발해서 현재는 전 세계 표준이 된 3D 데이터의 처리 스펙이자 오픈 소스 프레임워크입니다(ref5). Isaac Sim에서는 로봇, 컨베이어 벨트, 조명, 심지어 물리 법칙 설정까지 모든 것이 USD 파일입니다. Isaac Sim에서는 기본적으로 지원되는 USD도 Omniverse Kit의 맥락에서는 ‘확장’이라고 볼 수 있습니다(ref6). 내부적으로 Isaac Sim은 omni.usd.core라는 확장을 로드하여 USD 라이브러리에 연결합니다(ref7).

Python으로 Isaac Sim을 켜고 스트리밍하기

앞서 레고의 비유를 통해 설명했듯, Isaac Sim은 수많은 확장 기능(Extension) 브릭들이 모여 만들어진 거대한 세트입니다. 우리가 Python 스크립트에서 시뮬레이션을 제어하기 위해 가장 먼저 만나는 도구가 바로 SimulationApp입니다. 이 클래스는 파이썬 환경에서 Omniverse Kit이라는 거대한 엔진을 깨우고 필요한 브릭들을 한데 모아주는 일종의 '가이드북' 역할을 수행합니다. 하지만 별다른 설정 없이 단순히 SimulationApp을 호출하는 것만으로는 스트리밍 기능을 활성화할 수 없는데, 여기서부터 많은 개발자가 첫 번째 시행착오를 겪게 됩니다.
가장 먼저 마주하게 되는 벽은 간단한 코드에서 시작됩니다. "헤드리스 모드(화면 없음)로 스트리밍을 켜겠다"는 의도를 담아 {"headless": True, "livestream": 1}이라는 설정을 넘기고 스크립트를 실행해 봅니다.
PUBLIC_IP=100.65.116.2 uv run main.py
Bash
복사
100.65.116.2는 제가 Tailscale을 통해 받은 VM의 IP 주소입니다.
하지만 분명 CLI 환경에서 isaacsim.exp.full.streaming 명령어로 실행했을 때는 잘 나오던 화면이, 파이썬 스크립트로는 도무지 연결되지 않는 현상을 발견하게 됩니다.
이 문제의 실마리는 SimulationApp이 인스턴스화될 때 콘솔에 출력되는 실행 로그에 숨어 있습니다. 로그를 자세히 뜯어보면 Isaac Sim이 내부적으로 어떤 인자를 가지고 엔진을 실행하는지 확인할 수 있는데, 그 내용은 대략 다음과 같습니다.
['.../simulation_app.py', '.../isaacsim.exp.base.python.kit', '--/app/tokens/exe-path=...', '--/persistent/app/viewport/displayOptions=3094', ..., '--portable', '--no-window', '--/app/window/hideUi=1']
Bash
복사
이것을 스트리밍 클라이언트로 확인을 성공했던 CLI 명령과 비교해 봅시다.
isaacsim isaacsim.exp.full.streaming --/app/livestream/publicEndpointAddress=100.65.116.2
Python
복사
둘을 비교해 보면 두 가지 중요한 차이점을 발견할 수 있습니다. 첫째는 스트리밍 접속을 위해 필수적인 publicEndpointAddress 옵션이 통째로 빠져 있다는 것이고, 둘째는 실행의 기반이 되는 설계도 파일이 full.streaming이 아닌 base.python.kit으로 잡혀 있다는 점입니다.
여기서 앞서 언급한 레고의 비유를 다시 가져와 보겠습니다. isaacsim.exp.base.python.kit은 이름 그대로 아주 기초적인 브릭들만 들어있는 '스타터 키트'입니다. SimulationApp을 그냥 실행하면 기본적으로 스타터 키트를 가져옵니다. 이 상자 안에는 스트리밍을 처리하는 "통신 모듈 브릭"이나 "WebRTC 브릭"이 들어있지 않습니다. 반면 우리가 원했던 isaacsim.exp.full.streaming.kit은 스트리밍에 필요한 모든 특수 브릭이 포함된 '풀 패키지 상자'입니다. 즉, 아무리 코드로 스트리밍을 켜라고 명령해도, 베이스가 되는 상자 자체가 부실하면 기능은 작동하지 않는 것입니다.
하지만 안타깝게도 이 옵션들을 어떻게 파이썬에서 정확히 전달해야 하는지에 대한 친절한 공식 문서는 찾기 어렵습니다. 결국 우리는 SimulationApp의 소스코드를 직접 열어 내부를 들여다보는 길을 선택하게 됩니다. 여기서 SimulationApp 구현체의 _start_app 메서드를 살펴보면 다음과 같은 로직을 발견할 수 있습니다.
# SimulationApp 내부 코드 발췌 def _start_app(self) -> None: # ... (생략) args = [os.path.abspath(__file__)] if "experience" in self.config: args.append(f'{self.config["experience"]}') # ... (중략) # Add additional command line arguments extra_args = self.config.get("extra_args", []) if isinstance(extra_args, list): args.extend(self.config.get("extra_args", []))
Python
복사
코드를 분석해 보면 두 가지 핵심 열쇠를 얻을 수 있습니다. 첫째, experience라는 인자를 통해 우리가 원하는 '풀 패키지 상자(.kit 파일)'의 경로를 직접 지정해 줄 수 있다는 것. 둘째, extra_args라는 리스트를 통해 CLI에서 입력하던 주소 설정값을 엔진에 직접 찔러 넣어줄 수 있다는 점입니다.
우리가 해야 할 일은 1) 라이브러리 경로 얻고 2) full.streaming 키트를 선택하고 3) 엔진에 직접 IP 주소 인자를 넘겨주는 것입니다. 그렇게 탄생한 완성형 코드는 다음과 같습니다.
import os from pathlib import Path from isaacsim import SimulationApp # 1. SimulationApp을 import하면 ISAAC_PATH가 환경 변수에 자동으로 등록됩니다. wow~ isaac_path = os.environ.get("ISAAC_PATH") assert isaac_path isaac_path = Path(isaac_path) # 2. full.streaming 키트를 선택합니다. streaming_kit_path = isaac_path / "apps" / "isaacsim.exp.full.streaming.kit" # 3. 실행할 때 PUBLIC_IP=100.65.116.2 python main.py 와 같이 환경변수를 넘겨줍니다. public_ip = os.environ.get("PUBLIC_IP") assert public_ip config = { "headless": True, "livestream": 1, "extra_args": [f"--/app/livestream/publicEndpointAddress={public_ip}"], } simulation_app = SimulationApp( launch_config=config, experience=streaming_kit_path.as_posix(), ) # 이후 시뮬레이션 루프
Python
복사
이제 다시 스크립트를 실행해 보면, 이전과는 확연히 다른 로그가 우리를 반깁니다. 실행 인자 리스트에 우리가 원했던 ...full.streaming.kit--/app/livestream/publicEndpointAddress=100.65.116.2가 정확히 박혀 있는 것을 확인할 수 있습니다. 스트리밍이 정상적으로 시작되고, 클라이언트로 접속할 수 있게 됩니다. 클라이언트로 접속했을 때 다음과 같이 스트리밍이 되는 것을 확인합니다.

시뮬레이션에 창고 불러오기

이제 '창고(Warehouse)'를 띄워 볼 차례입니다. 빈 공간에 단순히 상자 하나를 띄우는 것이 아니라, NVIDIA가 제공하는 고퀄리티의 창고 자산(Asset)을 불러와 실제와 흡사한 시뮬레이션 환경을 구축해 보겠습니다.
스트리밍 창을 열었을 때 가장 먼저 마주하는 것은 아마도 끝없는 회색 격자판일 것입니다. 이 빈 공간을 채우기 위해 우리는 앞서 언급했던 USD(Universal Scene Description) 파일을 로드해야 합니다. Isaac Sim은 전 세계 사용자가 공통으로 사용하는 자산들을 Nucleus라는 클라우드 서버 혹은 로컬 캐시 서버를 통해 관리합니다. 이를 위해 우리는 먼저 자산이 저장된 뿌리 경로(assets_root_path)를 찾아내야 합니다.
... import omni.usd from isaacsim.core.api import World from isaacsim.storage.native import get_assets_root_path # 4. 에셋 루트가 어디 있는지 물어본다. 로컬인지 클라우드인지는 함수에게 맡깁니다. assets_root_path = get_assets_root_path() if not assets_root_path: print("Error: Nucleus 서버 또는 기본 에셋 경로를 찾을 수 없습니다.") simulation_app.close() exit() # 5. 우리가 불러올 창고 설계도(USD)의 고유 경로를 `assets_root_path`가 반환한 클라우드/로컬 경로 뒤에 붙입니다. # 참고로 클라우드 경로일수도 있기 때문에 Pathlib을 사용하지 않은 것입니다! warehouse_usd_path = ( assets_root_path + "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" )
Python
복사
경로를 준비했으니, 이제 omni.usd.get_context().open_stage()라는 명령을 통해 설계도를 펼칩니다.
... print(f"Loading Warehouse from URI: {warehouse_usd_path}") omni.usd.get_context().open_stage(warehouse_usd_path) for _ in range(60): simulation_app.update()
Python
복사
USD 파일은 텍스트 몇 줄이 아니라 수많은 3D 메쉬와 텍스처 정보가 담긴 무거운 데이터입니다. 엔진이 이 데이터를 메모리에 올리고 렌더링 준비를 마칠 때까지는 물리적인 시간이 필요합니다. 그래서 우리는 설계도를 펼친 직후, 앱을 여러 번 업데이트(simulation_app.update())하며 엔진이 데이터를 완전히 소화할 수 있도록 기다려 주어야 합니다.
창고가 무사히 세워졌다면, 이제 World 객체를 생성합니다. World는 시뮬레이션 내부의 시간 흐름과 물리 법칙을 총괄합니다. world.reset()을 통해 초기 상태를 잡고 나면, 드디어 우리는 무한 루프 안에서 시뮬레이션을 한 걸음씩(world.step) 전진시키게 됩니다.
... world = World(stage_units_in_meters=1.0) world.reset() # 앱이 실행 중인 동안 계속해서 물리 연산과 렌더링을 수행합니다. while simulation_app.is_running(): # render=True 설정을 통해 계산된 결과를 스트리밍 화면으로 전송합니다. world.step(render=True) simulation_app.close()
Python
복사
이 코드가 실행되면, 드디어 웹 브라우저나 스트리밍 클라이언트를 통해 world.step()이 반복될 때마다 실시간으로 빛이 반사되고 물리 법칙이 작동하는 디지털 트윈이 클라이언트로 전송됩니다.
checkpoint

창고에 로봇 팔 소환하기

우리는 앞으로 창고에 다양한 것들을 소환하겠지만, 가장 먼저 물건을 집어들 수 있는 기구를 배치해 보고 싶네요. 연습용 로봇 팔 (-10, 1, 0)에 불러와 봅시다. (-10, 1, 0)은 그냥 경험적으로 찾은 창고의 중앙입니다. 그 전에 몇 개념만 습득하고 갑시다.

USD 기본

USD는 프림이라는 개념적 요소로 구성됩니다. 프림 그 자체는 아무것도 없는 빈 상자와 같습니다. 이 상자에 '어떤 속성(Attribute)을 담느냐'에 따라 정체성이 결정됩니다.
위치, 회전, 크기 정보만 가짐 → Xform 프림
점의 위치(Points), 면의 구성(Face Vertex Counts) 등을 가짐 → Mesh 프림
이 프림들이 계층적으로 쌓여서 로봇이 되고, 창고가 되고, 물리 법칙이 작동하는 시뮬레이션 환경이 됩니다.
효율을 위해 USD 모델 파일은 '복사'되지 않고 '참조'됩니다. 물류창고에 n개의 지게차가 있다고 할 때, 원본 지게차 파일이 수정되면 이를 참조하는 지게차에 자동으로 반영됩니다. 참조를 유지하되 특정 상황에서만 바꾸고 싶은 경우에 대비하여 '레이어'라는 개념도 있습니다. 이를 통해 원본 파일을 건드리지 않고도 특정 씬에서만 지게차의 색상을 바꾸거나 위치를 변경하는 비파괴적(Non-destructive) 편집이 가능합니다.

SimReady

NVIDIA는 SimReady(Simulation Ready)라는 에셋 라이브러리를 제공합니다. 이 에셋들은 단순 3D 모델이 아니라, 물리 속성(질량, 마찰계수)과 의미론적 라벨(Semantic Label, 예: "이것은 팔레트이다")이 이미 적용되어 있습니다. 의미론적 라벨(Semantic Label)이란 말 그대로 “이 3D 데이터 덩어리는 '지게차(Forklift)'라는 카테고리에 속한다.” 혹은 “이 영역은 '바닥(Floor)', 저 영역은 '벽(Wall)'이다.” 따위의 메모입니다. 모델 학습을 위한 합성 데이터를 생성하는 데 유용하게 사용될 수 있을 것입니다.
우리가 작성한 코드에서 full_warehouse.usd를 불러오는 행위는 Nucleus라는 인프라를 이용해 데이터를 가져오는 과정입니다. 하지만 만약 우리가 불러온 에셋이 SimReady 규격이 아니라면 어떤 일이 벌어질까요?
아마도 공장에 배치한 선반은 중력을 무시하고 공중에 떠 있거나, 로봇이 벽을 뚫고 지나가는 기괴한 광경을 목격하게 될 것입니다. 개발자는 일일이 모든 물체의 무게를 입력하고 마찰력을 계산해야 하는 고된 '막노동에 시달리게 됩니다.
하지만 NVIDIA가 Nucleus 서버를 통해 제공하는 SimReady 에셋들은 이 과정을 생략해 줍니다. SimReady 에셋을 활용한다는 의미는 단순히 경로를 적는 것을 넘어, 이미 물리 법칙과 라벨링이 끝난 데이터를 신뢰하고 가져온다는 뜻입니다.
warehouse_usd_path = assets_root_path + "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd"
Python
복사
이 설계도(USD) 안에는 "콘크리트 바닥의 마찰계수는 0.6이다", "철제 선반의 무게는 50kg이다"라는 정보가 SimReady 규격에 맞춰 이미 기록되어 있습니다. 덕분에 우리는 world.step()을 실행하자마자 별다른 설정 없이도 실제 물리 법칙이 지배하는 정교한 공장 시뮬레이션을 곧바로 수행할 수 있는 것입니다.
전체 코드는 아래와 같습니다. 코드를 보면 prim_path="/World/Fancy_Franka"를 확인할 수 있는데, 이것이 바로 시뮬레이션 세계 안에서 이 로봇이 위치할 고유한 주소를 의미합니다.
import os from pathlib import Path import numpy as np from isaacsim import SimulationApp # 시뮬레이션 앱 설정 (기존 설정 유지) public_ip = os.environ.get("PUBLIC_IP") assert public_ip isaac_path = Path(os.environ.get("ISAAC_PATH")) streaming_kit_path = isaac_path / "apps" / "isaacsim.exp.full.streaming.kit" config = { "headless": True, "livestream": 1, "extra_args": [f"--/app/livestream/publicEndpointAddress={public_ip}"], } simulation_app = SimulationApp( launch_config=config, experience=streaming_kit_path.as_posix(), ) import omni.usd from isaacsim.core.api import World from isaacsim.robot.manipulators.examples.franka import Franka from isaacsim.storage.native import get_assets_root_path # 창고 환경 로드 assets_root_path = get_assets_root_path() warehouse_usd_path = ( assets_root_path + "/Isaac/Environments/Simple_Warehouse/full_warehouse.usd" ) print(f"Loading Warehouse from URI: {warehouse_usd_path}") omni.usd.get_context().open_stage(warehouse_usd_path) # World 설정 world = World(stage_units_in_meters=1.0) # Franka 로봇 소환 franka = world.scene.add( Franka( prim_path="/World/Fancy_Franka", name="fancy_franka", position=np.array([-10.0, 1.0, 0.0]), ) ) # 시뮬레이션 초기화 (딱 한 번 실행) world.reset() print("Streaming Ready! Franka robot spawned.") # 앱이 실행 중인 동안 계속해서 물리 연산과 렌더링을 수행합니다. while simulation_app.is_running(): world.step(render=True) simulation_app.close()
Python
복사
parse me : 언젠가 이 메모에 쓰이면 좋을 것 같은 재료들입니다.
1.
from : 이 메모에 쓰인 생각을 만든 과거의 생각들과 연관관계를 설명합니다.
supplementary : 이 메모에 작성된 생각을 뒷받침하는 새로운 메모입니다.
1.
None
opposite : 이 메모에 작성된 생각과 대조되는 새로운 메모입니다.
1.
None
to : 이 메모에 작성된 생각으로부터 발전된 생각의 메모입니다.
1.
2.
3.
ref : 생각에 참고한 자료입니다.
프로젝트메모 템플릿 버전 2025.11.16