작성일: 2026-06-05 (최종 갱신 2026-06-08)
환경: Ubuntu 24.04 / Python 3.12 / NVIDIA RTX A5000 ×4 / CUDA 13.0 (드라이버) / Isaac Sim 5.1.0 (Docker)
최종 결과: Isaac Sim 5.1에서 GS+Mesh 렌더링 → 로봇 보행용 평탄 충돌 환경 생성 → Jackal 휠로봇 WASD 텔레옵 주행 → PhysX LiDAR + RGB(GS) 카메라를 부착해 ROS2로 RViz2 시각화까지 성공
PortalCam(XGRIDS)이 생성한 USD 파일은 Gaussian Splatting 데이터를 ParticleField3DGaussianSplat이라는 스키마로 저장한다. 이 스키마는 OpenUSD 26.03(2026년 3월)에서 공식 표준으로 채택되었으며, Isaac Sim 5.1(NuRec 렌더러 포함)부터 공식 지원된다.
목표: 260521_ERTI 1.usd(GS)와 260521_ERTI 1.obj(Mesh)를 Isaac Sim 5.1에서 함께 로드하여 GS 스플래팅과 메쉬를 동시에 확인한다.
USDZ/USDZ_ETRI1/
├── 260521_ERTI 1.usd (694.7 MB) — GS 데이터 인라인 포함
└── 260521_ERTI 1.obj ( 1.7 MB) — Mesh
⚠️ 당초 todo.md에는 경로가USDZ_ERTI1/lcc-usd-result/로 되어 있었으나, 실제로는USDZ_ETRI1/하위에 바로 파일이 있었다. (ETRI ≠ ERTI, 하위 폴더 없음)
inspect_usd.py로 분석한 결과:
/World (Xform)
└── /World/Gaussians (Xform)
└── /World/Gaussians/gaussians (ParticleField3DGaussianSplat)
| 항목 | 값 |
|---|---|
| upAxis | Y (Isaac Sim 기본값 Z와 다름) |
| metersPerUnit | 1.0 |
| GS splat 수 | 3,086,676 |
| 외부 에셋 참조 | 없음 (모두 인라인) |
| Mesh Prim | 없음 (OBJ 별도 파일) |
GS 속성 (ParticleField3DGaussianSplat 기준):
| 속성명 | 타입 | 비고 |
|---|---|---|
positions |
point3f[3M] | 선형 좌표 |
orientations |
quatf[3M] | USD 쿼터니언 (w,x,y,z) |
scales |
float3[3M] | 선형 스케일(m), log 변환 필요 |
opacities |
float[3M] | 0~1 선형, logit 변환 필요 |
radiance:sphericalHarmonicsCoefficients |
float3[49M] | degree-3 SH, 16계수×3채널 |
이유: USD 파일을 Python에서 읽으려면 usd-core 패키지가 필요하다. 시스템에 pip조차 없었다.
작업:
get-pip.py로 pip 설치usd-core 26.5,numpy 2.4.6설치
결과: 설치 성공. usdchecker CLI는 usd-core Python 패키지에 미포함이므로 Step 7 유효성 검사에서 Python API로 대체.
라이선스 검토: usd-core는 TOST-1.0(Apache 2.0 기반) — 내부 연구 목적 사용 문제없음. 특허 조항 주의.
이유: PortalCam USD의 내부 구조를 파악해야 변환 방식을 결정할 수 있다.
작업: inspect_usd.py 실행 → docstring 백슬래시 유니코드 이스케이프 버그 수정 후 실행
결과: ParticleField3DGaussianSplat 확인, GS 데이터 속성 목록 파악.
→ Case B (표준 PLY 변환 필요) 판별
이유(계획): 공백 포함 파일명을 정리하고 외부 참조 경로를 수정하려 했다.
결과: USD 파일이 외부 에셋 참조가 전혀 없고(GS 데이터 전부 인라인), 원본 경로로도 잘 동작하므로 불필요 → 건너뜀.
이유: 표준 3DGS PLY 포맷으로 변환해야 3DGRUT(NuRec 변환 도구)에서 입력으로 사용할 수 있다.
작업: gs_to_ply.py 대폭 수정
ParticleField3DGaussianSplat타입 인식 추가- 속성명 매핑 (기존 표준 primvar 이름과 다름)
- 스케일: 선형 →
log(scale)변환 - 불투명도: 0~1 →
logit(opacity)변환 - SH 계수:
(N×16, 3)→ DC + f_rest (채널 우선 재배열) - 쿼터니언:
Gf.Quatf.real/.imaginary로 명시 추출
결과: output/USDZ_ETRI1/260521_ERTI_1_gs.ply 생성 (731 MB, 3,086,676 splats, 62 속성)
이유(계획): GS 원본 USD + OBJ를 묶어 USDZ로 패키징하면 Isaac Sim에서 열 수 있을 것이라 예상했다.
작업:
build_combined_usd.py수정: 경로, GS 타입 감지, Y-up→Z-up 회전 적용package_usdz.py로 USDZ 생성 (1,426 MB, 원본 USD + OBJ + PLY 포함)UsdUtils.CreateNewUsdzPackage사용
결과:
- USDZ 자체는 생성됨 (
usdchecker검사: Errors/Warnings 없음) - Isaac Sim 4.5에서 로드 시 하위 prim이 표시되지 않거나 HydraEngine 에러 발생
- 원인 분석:
OBJ 참조가 usd-core에서 실패 → 전체 Stage 로딩에 영향, 캐시 문제, USDZ 내부 경로(0/vs1/) 등 복합적 원인 - PLY 제거, 파일명 변경, zipfile 직접 패키징 등 여러 시도를 했으나 모두 불안정
- 근본 원인: Isaac Sim 4.5.0이
ParticleField3DGaussianSplat스키마를 모름 (내부 USD 버전 0.24.5, 해당 스키마는 USD 26.03에서 표준화)
→ USDZ 패키징 방식 포기. combined.usda를 직접 여는 방식으로 전환.
이유: USDZ가 불안정하므로 USDA 파일을 직접 열어본다.
작업: output/USDZ_ETRI1/260521_ERTI_1_combined.usda 직접 오픈
결과:
- Mesh(
/World/Mesh): OBJ 로드 성공, 뷰포트에 Mesh 렌더링 ✅ - GaussianSplats(
/World/GaussianSplats/Gaussians/gaussians): Stage에 로드됨, 하지만 뷰포트에 렌더링 안 됨 ❌ - 원인: Isaac Sim 4.5.0이
ParticleField3DGaussianSplat을 렌더링하는 Hydra Scene Delegate가 없음
Isaac Sim 4.5 환경에서 GS를 렌더링하기 위해 여러 방법을 시도했다.
이유: Isaac Sim Extension Manager에서 omni.gsplat.viewport를 발견해 설치 시도.
결과: "Failed to solve extension dependency" 오류. 실제로는 이 익스텐션이 Python Extension 예제 템플릿일 뿐, 실제 GS 렌더러가 아님. Isaac Sim 4.5 컨테이너에는 GS 관련 익스텐션이 전혀 없음(확인: 두 컨테이너 모두 427개 extscache 동일, GS 관련 없음).
이유: PLY를 드래그 앤 드롭으로 Isaac Sim에 로드하면 GS로 렌더링될 것이라 예상.
결과: Isaac Sim이 PLY를 변환해 260521_ERTI_1_gs.usd(672 B) 생성했으나, 파일 내용은 빈 Xform만 있음 → Isaac Sim 4.5가 3DGS PLY 포맷을 인식하지 못함.
이유: GS 렌더링이 불가능하면 포인트 클라우드라도 보여주자.
결과: 260521_ERTI_1_pointcloud.usda 생성(280 MB). Isaac Sim에서 열면 수백만 개의 거대한 구체(sphere)로 렌더링됨. 배경 GS의 scale이 커서 수 미터짜리 구로 보임. 색상이나 공간 구조를 알아볼 수 없어 실용적 가치 없음.
이유: PointInstancer로 billboard quad를 만들면 GS와 유사한 효과를 낼 수 있지 않을까.
결론: 분석 결과 포기. GS 스플래팅의 핵심 요소(매 프레임 카메라 기준 back-to-front 정렬, 2D Gaussian 투영, SH 시점 의존 색상)를 PointInstancer로 재현 불가능. 전혀 다른 렌더링 방식.
| 이유 | 내용 |
|---|---|
| 내부 USD 버전 | 0.24.5 (ParticleField3DGaussianSplat 미지원) |
| GS Hydra Delegate | 없음 |
| omni.gsplat 공식 지원 | Isaac Sim 5.0 이상 (Kit 107.3+) |
이유: Isaac Sim 5.1은 NuRec 렌더러를 통해 GS를 공식 지원한다는 정보를 확인.
작업: nvcr.io/nvidia/isaac-sim:5.1.0 pull (별도 NGC 로그인 불필요, 이미 인증됨)
확인 내용: Isaac Sim 5.1 내부 USD 버전도 0.24.5로 ParticleField3DGaussianSplat 스키마 자체는 모름. 하지만 NuRec OmniNuRecFieldAsset 스키마로 변환된 USDZ는 렌더링 가능.
→ 3DGRUT 변환 도구로 PLY → NuRec USDZ 변환 필요.
이유: 3DGRUT(nv-tlabs/3dgrut)는 NVIDIA 공식 오픈소스 도구로, 표준 3DGS PLY를 Isaac Sim 5.x에서 렌더링 가능한 NuRec USDZ 포맷으로 변환한다.
3DGRUT 특징:
- 출력:
OmniNuRecFieldAsset스키마 기반 USDZ (Isaac Sim 5.0+ 전용) - CUDA 필수 (GPU 연산), PyTorch 의존
작업:
- 저장소 클론:
git clone --recursive https://github.com/nv-tlabs/3dgrut.git - uv 설치 (pip 대안 패키지 매니저)
- Docker 이미지 빌드 (
CUDA_VERSION=12.8.1, A5000 sm_86 호환)- CUDA 13.0 드라이버지만 툴킷은 미설치 → Docker에서 CUDA 12.8.1 포함 빌드
- 소요 시간:
1030분 (Kaolin, tiny-cuda-nn 등 CUDA 커널 컴파일)
라이선스 점검 및 조치 (2026-06-22) — 처음에 놓쳤다가 사용자 지적으로 수정:
- 증상/발견: 원본 3dgrut Dockerfile이 **Miniconda3(
repo.anaconda.com)**를 설치하고conda create/conda install이 채널 미지정으로 Anacondadefaults채널에서 패키지를 받음. 실측 결과conda list --show-channel-urls에서 80개가defaults(python, cmake, ninja, gcc 툴체인 등). - 리스크:
defaults(repo.anaconda.com)는 Anaconda Terms of Service 적용 → 200명 초과 조직 상업 사용은 유료(Anaconda Business). ETRI(대규모 정부출연연)는 임계 초과 가능 → 회색지대, 법무 확인 필요. conda 도구(BSD)·conda-forge·NVIDIA 채널·PyPI는 무관. - 조치: Miniconda3 → Miniforge3로 교체(기본 채널 conda-forge).
install_env.sh의conda create/conda install cuda...에-c conda-forge명시. 재빌드 후 검증 →defaults0개(conda-forge 81 / nvidia 66 / pypi 193). 동일 태그3dgrut:cuda128로 대체. - 출력물 영향 없음: 라이선스 이슈는 저장소 사용 행위에 대한 것 → 이미 만든
_nurec.usdz는 재생성 불필요. 향후 빌드/변환만 깨끗한 이미지 사용. - 검증 커맨드:
docker run --rm 3dgrut:cuda128 conda run -n 3dgrut conda list --show-channel-urls | grep -c defaults→ 0.
변환 실행:
docker run --rm --gpus '"device=1"' \
-v /home/zeozeo/git/usd2usdz:/usd2usdz \ # /workspace 가 아닌 별도 경로 필수
3dgrut:cuda128 \
conda run -n 3dgrut \
python -m threedgrut.export.scripts.ply_to_usd \
/usd2usdz/output/USDZ_ETRI1/260521_ERTI_1_gs.ply \
--output_file /usd2usdz/output/USDZ_ETRI1/260521_ERTI_1_nurec.usdz
⚠️ 마운트 경로가/workspace이면 3DGRUT 소스코드를 덮어써threedgrut모듈을 찾지 못함. 반드시/usd2usdz등 다른 경로로 마운트해야 한다.
결과: 260521_ERTI_1_nurec.usdz 생성 (348 MB)
아카이브 내용:
default.usda — 루트 레이어 (upAxis=Z, OmniNuRecFieldAsset 참조)
260521_ERTI_1_nurec.nurec — GS 데이터 (347 MB)
gauss.usda — Volume prim 정의
Isaac Sim 5.1에서 확인: GS 스플래팅 렌더링 ✅
이유: nurec.usdz에 OBJ Mesh를 함께 포함해 하나의 파일로 만들려 했다.
작업: zipfile로 nurec.usdz에 OBJ를 추가, default.usda에 Mesh prim 참조 추가.
결과: Isaac Sim 5.1에서 열면 GS도, Mesh도 모두 렌더링 안 됨.
원인: OBJ 참조 실패가 전체 Stage 로딩을 방해하는 것으로 추정.
→ USDZ에 OBJ를 넣는 방식 포기.
이유: USDZ 아카이브 내 경로 문제를 피하기 위해, 디스크의 USDA 파일에서 nurec.usdz(GS)와 OBJ(Mesh)를 별도로 참조하는 방식으로 전환.
좌표계 정렬 과정:
- 처음에는 OBJ에
rotateX=90적용 (Y-up → Z-up 변환 의도) - Isaac Sim 5.1에서 열면 GS와 Mesh의 좌표계가 안 맞음
- 원인: 3DGRUT가 PLY를 변환할 때 좌표 변환을 적용하지 않고 Y-up 데이터 그대로 저장
- → Mesh에서도 회전 제거하여 두 좌표계 일치
최종 USDA:
#usda 1.0
(
defaultPrim = "World"
metersPerUnit = 1
upAxis = "Z"
)
def Xform "World"
{
def Xform "GaussianSplats" (
prepend references = @./260521_ERTI_1_nurec.usdz@
)
{
}
def Xform "Mesh" (
prepend references = @./260521_ERTI_1_mesh.obj@
)
{
}
}
Isaac Sim 5.1 실행 시 주의사항:
/home/zeozeo디렉토리 퍼미션이0750이면 컨테이너에서 접근 불가chmod o+rx /home/zeozeo로 해결
결과: GS 스플래팅 + Mesh 동시 렌더링 ✅
output/USDZ_ETRI1/
├── 260521_ERTI_1_nurec_mesh.usda ← Isaac Sim 5.1에서 이 파일을 열면 됨
├── 260521_ERTI_1_nurec.usdz ← GS (NuRec 포맷, 348 MB)
└── 260521_ERTI_1_mesh.obj ← Mesh (1.7 MB)
# 중간 산출물 (참고용)
output/USDZ_ETRI1/
├── 260521_ERTI_1_gs.ply ← 표준 3DGS PLY (3DGRUT 입력용, 731 MB)
└── 260521_ERTI_1_combined.usda ← Isaac Sim 4.5용 (Mesh만 렌더링, GS 불가)
| 스크립트 | 수정 내용 |
|---|---|
inspect_usd.py |
docstring 백슬래시 유니코드 이스케이프 버그 수정 |
gs_to_ply.py |
ParticleField3DGaussianSplat 전용 추출 함수 추가, logit/log/SH/쿼터니언 변환 |
build_combined_usd.py |
실제 경로 수정, GS 타입 감지 로직 추가 (결과적으로 미사용) |
package_usdz.py |
수정 없이 사용 (결과적으로 미사용) |
gs_to_pointcloud_usd.py |
신규 작성 (UsdGeom.Points 변환, 실용 가치 없어 미사용) |
- OpenUSD 26.03(2026.03)에서 AOUSD가 공식 표준으로 채택
- PortalCam(XGRIDS)이 이 표준을 선도적으로 채택하여 USD 출력에 사용
- Isaac Sim 4.5.0 내부 USD 버전(0.24.5)은 이 스키마를 모름 → 렌더링 불가
- Isaac Sim 5.1에서도 직접 렌더링 불가, NuRec 변환 도구(
3DGRUT)를 통해야 함
| 버전 | 내부 USD | GS 지원 방식 |
|---|---|---|
| 4.5.0 | 0.24.5 | ❌ 없음 |
| 5.1.0 | 0.24.5 | 🔶 NuRec USDZ 경유 (3DGRUT 변환 필요) |
| 6.0 (Early Dev) | - | ✅ NuRec Fabric Scene Delegate 내장 |
3DGRUT의 PLY → USDZ 변환 결과물은 OmniNuRecFieldAsset 스키마를 사용한다. 이는 UsdVolVolume 기반의 Omniverse 전용 스키마로, Isaac Sim 5.0 이상(Kit 107.3+)에서만 NuRec 렌더러로 렌더링 가능하다.
- USDZ: ZIP 아카이브. 내부 경로 리졸버가 복잡하여 Isaac Sim에서 OBJ 참조 실패 시 전체 Stage 로딩이 방해받는 문제 발생.
- USDA: 파일시스템 직접 참조. 경로 문제 없이 안정적으로 로드됨. 최종적으로 USDA 방식 채택.
# ~/git/InternNav/run-isaac-sim-5.1.sh
xhost +local:root
docker run --name isaac-sim-5.1 --entrypoint bash -it \
--gpus '"device=1"' --cpus="12" --memory="60g" --shm-size="16gb" \
-e "ACCEPT_EULA=Y" -e "PRIVACY_CONSENT=Y" --rm --network=host \
-e DISPLAY \
-v $HOME/.Xauthority:/root/.Xauthority \
-v ~/docker/isaac-sim/cache/kit:/isaac-sim/kit/cache:rw \
-v ~/docker/isaac-sim/cache/ov:/root/.cache/ov:rw \
-v ~/docker/isaac-sim/cache/pip:/root/.cache/pip:rw \
-v ~/docker/isaac-sim/cache/glcache:/root/.cache/nvidia/GLCache:rw \
-v ~/docker/isaac-sim/cache/computecache:/root/.nv/ComputeCache:rw \
-v ~/docker/isaac-sim/logs:/root/.nvidia-omniverse/logs:rw \
-v ~/docker/isaac-sim/data:/root/.local/share/ov/data:rw \
-v ~/docker/isaac-sim/documents:/root/Documents:rw \
-v /home/zeozeo:/home/zeozeo \ # chmod o+rx /home/zeozeo 선행 필요
nvcr.io/nvidia/isaac-sim:5.1.0docker run --rm --gpus '"device=1"' \
-v /home/zeozeo/git/usd2usdz:/usd2usdz \
3dgrut:cuda128 \
conda run -n 3dgrut \
python -m threedgrut.export.scripts.ply_to_usd \
/usd2usdz/output/USDZ_ETRI1/260521_ERTI_1_gs.ply \
--output_file /usd2usdz/output/USDZ_ETRI1/260521_ERTI_1_nurec.usdzGS+Mesh 렌더링 성공 이후, 복원 메쉬가 울퉁불퉁해 로봇 보행이 불가능한 문제를 해결하고, 실제로 휠로봇을 주행시키고 LiDAR/카메라 센서 데이터를 RViz2에서 확인하기까지의 작업 기록.
이유: GS+스캔 메쉬를 Isaac Sim에 올렸으나 복원 메쉬 표면 노이즈가 심해, Jackal(휠) / RBQ10(사족)이 바닥에서 튀거나 걸려 주행/보행 불가.
핵심 원리: 물리 시뮬레이션에서 로봇이 밟고 부딪히는 것은 시각 메쉬가 아니라 *충돌 지오메트리(collision geometry)*다. → 울퉁불퉁한 비주얼(GS + 스캔 메쉬)은 그대로 두고, 로봇이 상호작용하는 충돌 레이어만 깨끗하게 분리한다. (사용자 결정: 충돌 범위 = 바닥 + 벽/장애물, 방식 = 평면 collision 분리, 추가 패키지 없이 numpy + usd-core만)
작업: make_collision_env.py 신규 작성. 입력 OBJ(Y-up)에서 평탄 바닥 collider + 벽/장애물 collider를 분리 생성.
주요 함수:
parse_obj()—v/f v/vt/vn방어적 파싱, >3각형 fan triangulatedetect_levels()— 면 법선이 수직축을 향하는(up-facing) 삼각형의 중심 높이를 면적가중 히스토그램 → 바닥 평면 검출build_collision_geometry()— 바닥면/벽면 분리, 정점 압축compute_spawn()— 바닥의 열린 지점(장애물 없는 곳) + PCA로 yaw 산출 → 로봇 스폰 지점/방향 결정write_collision_usdc()— 평평한 슬랩(slab) 바닥 + 벽 메쉬 collider, 마찰 머티리얼,spawnPoint/spawnYaw커스텀 속성 저작
산출물: 260521_ERTI_1_collision.usdc(충돌 전용) + 260521_ERTI_1_robot.usda(Isaac 로드용 최상위 씬).
CLI:
python make_collision_env.py --index 1 \
--levels {auto,dominant,lowest} \
--floor-shape {slab,mesh} \
--friction 0.9 --floor-angle 60층/씬별 전략 (사용자 설명 반영):
| 인덱스 | 데이터 | 전략 |
|---|---|---|
| ETRI1 | 건물 실내 한 개 층 | 단일 평면 바닥(slab) |
| ETRI2 | 실내 2층→1층(뚫린 부분/계단) + 1층 실외 | 층별 평면 + 메쉬 계단 |
| ETRI3 | 실외(계단/울타리/나무 다수) | 지배적 지면만 평탄화 |
결과: 평탄 슬랩 바닥 + 벽 collider 생성 성공. 로봇이 평면 위에 안착, 벽에 막힘.
이유(증상): 생성한 _robot.usda를 Isaac Sim에서 열면 "축이 돌아가 있음"(로봇이 옆으로 누움).
원인 분석: Part 1에서는 비주얼 정렬만 보고 rotateX=90을 가정했으나, 실제 PortalCam 데이터는 Z-up이었다.
무조건 회전을 넣으면 물리 중력(-Z)과 바닥이 어긋난다.
작업: make_collision_env.py에 up-axis 자동 감지(데이터 분포로 판정)를 넣어 불필요한 회전 제거.
결과: 바닥이 수평(중력과 정렬)으로 로드됨. 로봇이 바로 섬.
증상: 슬랩 바닥에도 일부 작은 돌출(자갈 같은 것)이 남아 Jackal이 밟고 넘어지는 경우 존재. 결과: slab floor로 대부분 해소. 미세 잔여물은 실용상 허용(사용자 합의).
이유: 생성한 평탄 바닥이 실제로 로봇 주행에 적합한지 사람이 직접 조종해 검증.
작업: teleop_test.py 작성 — 키보드 WASD 텔레옵. Jackal 기본, 로봇별 프리셋(검증된 USD 경로/조인트명).
기본 로봇 /Isaac/Robots/Clearpath/Jackal/jackal.usd.
결과: GPU 헤드리스 + GUI에서 주행 검증. 1.5m 이상 직진 확인.
증상: 전진이 느리고 잘 안 움직임, 후진이 전진보다 빠름, 후진키 떼면 넘어질 듯 들썩. 원인 분석:
- 루프 안에서
sim_app.update()를 호출해 이중 스텝(double-step) 발생 → 물리 불안정. - 휠 드라이브 게인 미설정. 작업:
- 루프의
sim_app.update()제거 (world.step(render=True)만 사용). UsdPhysics.DriveAPI(angular)로 휠 드라이브 설정: stiffness=0, damping=1e3, maxForce=2e3.- 4륜 차동 구동:
vels = (lin + side*(ang*WHEEL_BASE/2))/WHEEL_R,WHEEL_R=0.098, WHEEL_BASE=0.37. 결과: 정상 주행. 4륜(Jackal)도 문제없이 구동.
가장 길고 디버깅이 많았던 작업. 목표: Jackal에 Ouster급 LiDAR + RGB 카메라를 달고 RViz2에서 센싱 확인. 단, 카메라에는 회색 복원 메쉬가 아니라 포토리얼 GS가 찍혀야 함.
이유: 처음엔 RTX LiDAR(Ouster OS 프리셋)를 시도. 핵심 발견(중요):
- RTX LiDAR/카메라는 렌더링 visibility를 공유한다. RTX LiDAR는 보이는 메쉬를 센싱.
- GS(NuRec)는 헤드리스 오프스크린 render product에는 렌더되지 않고, 인터랙티브 GUI 뷰포트에서만 렌더됨.
- 카메라에 GS만 찍으려면 VisualMesh를 숨겨야 하는데, 그러면 RTX LiDAR도 그 메쉬를 못 봐서 포인트가 사라짐 → visibility/투명도로 둘을 분리 불가.
작업/결과: → PhysX LiDAR(
RotatingLidarPhysX)로 전환. PhysX는 물리 충돌 지오메트리를 레이캐스트하므로 렌더링 visibility와 무관 → VisualMesh를 숨겨도(카메라=GS) LiDAR는 collider를 그대로 센싱. (사용자 승인: "PhysX 라이다로 전환")
결론: 지금 부착된 것은 실제 RTX Ouster가 아니라, PhysX LiDAR를 Ouster급(360°×수직30°, ~27k pts)으로 설정한 것. 실제 Ouster OS1-128 사양은
--lidar-vfov 45 --lidar-vres 0.35 --lidar-hres 0.35 --lidar-range 120로 근사 가능(레이 수 많아 무거움).
이유: 카메라 영상에 회색 복원 메쉬가 GS 위에 겹쳐 보이는 문제. 실패한 시도(기록):
- VisualMesh에
displayOpacity=0→ 머티리얼이 없어 무효. - 투명
UsdPreviewSurface머티리얼 부여 → 헤드리스(path-traced)에선 투명, RTX Real-Time GUI에선 불투명 렌더(효과 없음). (이 부분을 한때 "투명 적용됨"으로 잘못 보고함 — 사용자 지적으로 정정.) 작업(성공):UsdGeom.Imageable(VisualMesh).MakeInvisible()로 VisualMesh를 완전히 숨김. 결과: GUI 뷰포트에서 GS만 켜면 카메라(render product)에 GS가 찍힘(사용자가 캡처로 확인). PhysX LiDAR는 영향 없음. - 참고: VisualMesh 뷰 OFF 시 RViz에 포인트클라우드가 남아 보였던 것은 **이전 프레임의 잔상(decay 잔여)**일 뿐, 실제 LiDAR는 0이었음(사용자 확인).
증상: open_stage(_robot.usda)로 환경을 열면 PhysX LiDAR가 0 포인트.
원인 분석: open_stage 경로의 환경 내 physicsScene이 LiDAR 레이 쿼리를 방해.
작업: World(stage_units_in_meters=1.0) 생성 후 add_reference_to_stage(env, "/World/Scene")로 환경을 레퍼런스로 로드.
결과: collider 정상 레이캐스트, 1320 포인트 확인 → 이후 Ouster급 설정으로 ~27900까지.
증상: 로봇이 움직여도 센서가 스폰 위치에 고정.
원인: 센서를 아티큘레이션 루트 prim(/World/Jackal, 스폰 위치에서 안 움직임) 밑에 마운트.
작업: 움직이는 링크인 /World/Jackal/base_link 밑에 LiDAR/카메라 마운트.
결과: 센서가 로봇과 함께 이동.
증상: ros2 topic list엔 토픽이 보이는데 RViz/listener에 데이터가 안 옴. /clock, /tf 비어 있음.
원인 분석: Isaac 번들 FastDDS와 osrf humble FastDDS의 공유메모리(SHM) 전송 버전 불일치 → 컨테이너 간 데이터 전달 실패.
작업:
fastdds_udp.xml작성 — UDPv4 전용 프로파일(<useBuiltinTransports>false</useBuiltinTransports>).- 양쪽 컨테이너 모두
--ipc=host+-e FASTRTPS_DEFAULT_PROFILES_FILE=.../fastdds_udp.xml. - Isaac 실행 스크립트(
run-isaac-sim-5.1.sh)에--ipc=host추가. 결과: talker/listener로 컨테이너 간 통신 확인. 토픽 데이터 정상 수신.
증상: OmniGraph ROS2 퍼블리셔 노드가 standalone 스크립트에서 메시지를 안 내보냄.
작업: rclpy 직접 퍼블리시로 재작성. /clock(Clock), /tf(TFMessage), /point_cloud(PointCloud2), /rgb(Image)를 매 스텝 직접 publish.
make_pc2()— x/y/z FLOAT32, point_step 12, 비유한값 필터make_img()— rgb8tf_msg()— 쿼터니언 [w,x,y,z] → geometry_msgs [x,y,z,w] 결과: RViz에 LiDAR/카메라/TF 정상 표시.
증상: RViz에서 포인트클라우드가 한 번에 전체가 아니라 1/4씩 나눠 깜빡이며 채워짐.
원인 분석: PhysX LiDAR는 회전형이라 매 프레임이 회전의 일부(부분 스윕). 버퍼가 프레임마다 부분/전체로 바뀜
(폭 1320 ↔ 27900 교대 관찰). "전체 스윕일 때만 publish"하는 코드측 필터(npc >= 0.5*max_pts)는 깔끔히 안 걸러짐.
작업(최종 해법): 코드는 매 프레임 publish로 단순화하고, RViz sensors.rviz의 PointCloud2 Decay Time: 0.5 설정.
→ 회전 스윕이 0.5초간 누적돼 항상 전체 클라우드로 보임(회전 LiDAR 시각화의 정석).
결과: 깜빡임 해소. RViz 재시작(./run-rviz.sh)으로 새 설정 적용.
작업: 빈 GPU(0)에 --rm 헤드리스 Isaac 컨테이너로 RTX/PhysX 센서 실제 검증.
결과: 검증 후 임시 probe 파일 정리 + GPU 반납(16 MiB) — 사용자 표준 지침(검증 후 GPU 비우기) 준수.
증상: Jackal이 전·후진·회전 모두 기어가듯 느림. 처음엔 구동 토크 문제로 의심.
작업/진단:
-
루프에 RTF 측정 로그 추가:
t_wall0=time.time(),t_sim0=world.current_time기준으로RTF = (current_time - t_sim0)/(wall - t_wall0), sim fps도 함께 출력(60스텝마다). -
LiDAR 해상도를 바꿔가며 실측:
LiDAR 레이 수 RTF sim fps 27,900 (hres 0.4 × vres 1.0) 0.06 3.4 1,440 (hres 2.0 × vres 4.0) 0.21 12.8
결과(원인 확정):
- 구동 문제 아님. 시뮬레이션이 실시간의 6~21%로 느리게 돌아 슬로모션처럼 보인 것(로봇은 sim 기준 2 m/s로 정상 주행).
- PhysX LiDAR 레이캐스트가 최대 병목 (레이 1개당 ~7.7µs CPU). 27,900개면 한 스텝에 ~216ms.
- 단, 1,440개에서도 RTF 0.21에 그침 → GS(NuRec) 렌더가 RTF ~0.2의 바닥을 깔고 있음(레이를 더 줄여도 그 아래로 안 내려감).
튜닝(반영):
- LiDAR 기본 해상도를 균형값으로:
--lidar-hres 0.4 → 0.8(450×30 = 13,500 레이, RTF≈0.35~0.4 예상). 수직 30채널 유지. - 카메라 RGB 읽기(GPU→CPU)는 무거워
--cam-skip 4(N스텝당 1회만get_data+발행) 추가. - 명령 속도
--lin-speed 1.5→2.0, 휠 드라이브max_force 2e3→5e3(가속 개선).
증상: 직진만/후진만보다 직진+회전/후진+회전이 더 빠르게 느껴짐.
원인 분석: 구동 공식은 정상 차동구동(vels=(lin ± ang*WHEEL_BASE/2)/WHEEL_R)으로 전진 속도는 동일.
빠르게 느껴지는 이유는 (1) 회전 시 시야가 돌아 광학 흐름이 커서(특히 저RTF 슬로모션), (2) Jackal이 스키드-스티어(4륜 고정)라 제자리 회전 시 측면 슬립으로 약간의 전진 럴치 발생.
작업/결과: --ang-speed 3.0→2.0(172°/s→115°/s)으로 낮춰 회전 지배 완화. 구동 자체는 정상이므로 코드 수정 없음.
이유: ROS2 환경변수 4종 + python.sh 호출이 길어 매번 입력 번거로움.
작업: run-sensor-drive.sh 작성(컨테이너 안에서 실행). 환경변수 export 후 exec /isaac-sim/python.sh sensor_drive.py "$@"로 인자 그대로 전달. 인자 없으면 --index 1 기본.
결과: ./run-sensor-drive.sh [옵션]으로 한 번에 실행.
이유: 로봇이 멀리(y≈-15m) 주행하면 RViz 기본 시점(원점)을 벗어나 포인트클라우드가 화면 밖으로 나감. 시점이 로봇을 따라가게 하려 함.
작업: sensors.rviz의 Views/Current에 Target Frame: base_link 추가(Fixed Frame은 world 유지).
결과: 정상 동작. 시점이 로봇(base_link)을 따라가고 포인트클라우드도 정상 표시됨. (앞서 "안 보인다"고 본 것은 일시적 착시/오인이었고, 설정 자체는 처음부터 올바르게 작동하고 있었음.)
- RTX vs PhysX 센서: RTX LiDAR/카메라는 렌더 visibility(보이는 메쉬)를 센싱하고 서로 공유한다. PhysX LiDAR는 물리 collider를 레이캐스트해 visibility와 무관. → "카메라=GS, LiDAR=collider"처럼 둘을 분리하려면 PhysX LiDAR가 답.
- GS(NuRec) 렌더 범위: 인터랙티브 GUI 뷰포트에서만 렌더. 헤드리스 오프스크린 render product에는 안 나옴(검은 화면). 카메라로 GS를 얻으려면 GUI 세션 + 오프스크린 render product 조합.
- VisualMesh 투명화 불가: 투명 머티리얼은 RTX Real-Time에서 불투명 렌더됨. 숨기려면
MakeInvisible()(visibility=invisible)이 확실. - PhysX LiDAR 로딩:
open_stage(env)의 내장 physicsScene이 레이 쿼리를 막음.World()+add_reference_to_stage()로 로드해야 collider를 레이캐스트. - 센서 마운트: 아티큘레이션 루트 prim은 스폰 위치에 고정. 반드시
base_link(움직이는 링크) 밑에 마운트. - 이중 스텝 금지: standalone 루프에서
sim_app.update()와world.step()을 같이 부르면 물리 불안정.world.step(render=True)만 사용. - 컨테이너 간 ROS2(DDS): Isaac 번들 FastDDS ↔ osrf humble 간 SHM 버전 불일치로 데이터 전달 실패. **UDP 전용 프로파일 + 양쪽
--ipc=host**로 해결. - OmniGraph ROS2 퍼블리셔는 standalone python 스크립트에서 발행 안 될 수 있음 → rclpy 직접 발행이 안정적.
- 회전 LiDAR 시각화: 매 프레임은 부분 스윕. RViz
Decay Time을 한 회전 주기(~0.5s)로 두면 전체 스윕이 누적돼 보인다. - up-axis: PortalCam 데이터는 Z-up. 무조건
rotateX=90을 넣으면 물리에서 바닥이 어긋남 → 데이터 분포로 자동 감지. - PhysX LiDAR가 RTF를 지배: 레이캐스트는 CPU에서 레이당 ~7.7µs. 레이 수가 곧 비용 → 해상도가 주행 속도감(RTF)을 좌우. 27,900레이→RTF 0.06, 1,440레이→0.21. 시각화엔 13,500레이(hres 0.8) + Decay 0.5가 균형점.
- GS 렌더 RTF 바닥: GS(NuRec) 뷰포트 렌더만으로 RTF ~0.2가 한계. 라이다를 0에 가깝게 줄여도 그 위로 안 올라감. 주행감을 실시간에 가깝게 하려면 GS를 끄거나 카메라 render product를 빼야 함.
- 로봇이 느려 보이면 토크가 아니라 RTF부터 의심:
RTF = Δsim_time / Δwall_time를 찍어 확인. RTF≪1이면 렌더/센서 부하 문제(구동 무관). - 스키드-스티어 회전감: Jackal은 4륜 고정 스키드-스티어 → 제자리 회전 시 측면 슬립. 회전이 전진보다 빠르게 느껴지는 건 정상(광학 흐름 + 슬립 럴치).
ang-speed로 조절.
| 파일 | 내용 |
|---|---|
make_collision_env.py |
OBJ → 평탄 충돌 환경 생성. _collision.usdc(충돌) + _robot.usda(로드용). slab 바닥/벽 collider, 마찰, spawnPoint/spawnYaw |
teleop_test.py |
WASD 키보드 텔레옵. Jackal 기본 + 로봇 프리셋 |
sensor_drive.py |
센서+ROS2 통합. PhysX LiDAR(Ouster급) + RGB(GS) 카메라, World()+reference 로딩, VisualMesh invisible, rclpy 직접 발행, UDP 프로파일, RTF 로그 + cam-skip |
run-sensor-drive.sh |
sensor_drive.py 실행 런처(컨테이너 안). ROS2 env 4종 export + 인자 그대로 전달 |
fastdds_udp.xml |
UDP 전용 FastDDS 프로파일 (컨테이너 간 DDS 데이터 전달 필수) |
run-rviz.sh |
osrf/ros:humble-desktop 컨테이너로 RViz2 실행 (--network=host --ipc=host, UDP 프로파일) |
sensors.rviz |
RViz 설정. PointCloud2(/point_cloud, Best Effort, Decay Time 0.5), Image(/rgb), TF, Grid. Views Target Frame=base_link(로봇 추적, 11-12) |
ROBOT_PRIM="/World/Jackal"; BASE_LINK=ROBOT_PRIM+"/base_link"
SCENE_PRIM="/World/Scene"; VISUALMESH=SCENE_PRIM+"/Environment/VisualMesh"
LIDAR_OFFSET=(0,0,0.3); CAM_OFFSET=(0.2,0,0.25)
WHEEL_R=0.098; WHEEL_BASE=0.37
ROBOT_PATH="/Isaac/Robots/Clearpath/Jackal/jackal.usd"
# CLI 기본값: --lin-speed 2.0 --ang-speed 2.0 --lidar-vfov 30 --lidar-hres 0.8 --lidar-vres 1.0
# --lidar-range 100 --cam-skip 4 --cam-w 640 --cam-h 480 --headless --max-steps --show-visualmesh
# 밀도/속도 조절: 조밀(느림) --lidar-hres 0.4(27,900레이) / 성김(빠름) --lidar-hres 1.2 --lidar-vres 1.5# 1) Isaac (GUI) 컨테이너 안 — 런처로 센서 스크립트 실행(ROS2 env 자동 설정)
./run-sensor-drive.sh --index 1 # 옵션은 그대로 전달됨
# 2) 별도 터미널(호스트) — RViz2 (UDP 프로파일 + ipc=host)
./run-rviz.shRViz Fixed Frame은 world(필요시 lidar). PointCloud2/Image는 Best Effort QoS.
※ 멀리 주행해도 Views Target Frame=base_link로 시점이 로봇을 따라감(11-12).
ETRI 외 신규 PortalCam 데이터셋 2종(노은역 NOEUN, 월드컵경기장역 WC)을 변환하며 입력 포맷 차이와 대용량 NuRec의 USDZ 2GiB 오프셋 버그를 발견·해결했다.
이유: /USDZ/USDZ_NOEUN, /USDZ/USDZ_WC 데이터를 Isaac용 GS+메쉬로 변환.
입력 포맷 차이 (ETRI와 다름): .usd(인라인 GS) 없이 표준 3DGS PLY를 직접 제공.
USDZ_NOEUN/ point_cloud.ply(4.6G, 19,614,166 splat) + environment.ply(4.5M, 초희소 프리뷰) + noeun_station.obj(26M)
USDZ_WC/ point_cloud.ply(2.8G, 11,775,110 splat) + environment.ply(1.7M) + world_cup_stadium_station.obj(15M)
PLY 헤더가 f_dc_0..2, f_rest_0..44, opacity, scale_0..2, rot_0..3 (source=PortalCam) → 이미 표준 3DGS.
작업: ETRI의 gs_to_ply.py(USD→PLY) 단계 불필요 → point_cloud.ply를 3dgrut(Miniforge 클린 이미지)에 직접 투입.
docker run --rm --gpus '"device=0"' -v /home/zeozeo/git/usd2usdz:/usd2usdz 3dgrut:cuda128 \
conda run -n 3dgrut python -m threedgrut.export.scripts.ply_to_usd \
/usd2usdz/USDZ/USDZ_NOEUN/point_cloud.ply \
--output_file /usd2usdz/output/USDZ_NOEUN/noeun_station_nurec.usdz- OBJ는
output/USDZ_<name>/<base>_mesh.obj로 복사,_nurec_mesh.usda(upAxis=Z, GS+메쉬 참조) 작성. environment.ply(초희소 프리뷰)는 스킵. GPU는 --rm으로 반납.
결과: NOEUN noeun_station_nurec.usdz(2.2G, nurec 2,314MB), WC world_cup_stadium_station_nurec.usdz(1.3G, nurec 1,389MB) 생성. 둘 다 OmniNuRecFieldAsset/Z-up 정상.
이유: GS는 GUI에서만 렌더되어 헤드리스로 육안 확인 불가 → 좌표로 정렬을 객관 검증.
작업: GS 경계는 PLY 헤더 comment min/max(또는 splat 위치 표본 백분위), 메쉬 경계는 OBJ 정점 bbox로 계산해 비교.
결과(NOEUN): GS 중앙값 X=7.6/Y=−4.1, 메쉬 중심 X=9.0/Y=−4.9, Z바닥 −8.0/−7.5 일치, 축 교환 없음 → 동일 좌표계, 회전·스케일 보정 불필요.
- 주의: bbox 크기는 GS가 배경/floater로 훨씬 커 IoU가 낮게 나오지만, 이는 정렬 문제가 아니라 스플랫 분포 차이. 중앙값/축교환/Z바닥으로 판정할 것.
증상: NOEUN _nurec_mesh.usda를 Isaac에서 열면 Mesh만 회색으로 보이고, Mesh를 끄면 완전 검은 화면. GS가 렌더 안 됨. GPU VRAM 1.3GiB만 사용(2.3GB nurec 미로드).
원인 분석: Isaac Console 에러
In </World/GaussianSplats/gauss>: Could not open asset @gauss.usda@ ... introduced by @...noeun_station_nurec.usdz@
→ usdz 내부 gauss.usda(NuRec Volume 정의)를 못 엶 → Volume 미구성 → 검은 화면.
- 구조는 ETRI(정상)와 완전 동일, zip 무결성도 정상. 차이는 크기뿐.
- 진짜 원인: usdz 안에서 파일 순서가
default.usda → .nurec → gauss.usda인데,.nurec이 2.15 GiB(>2^31) 라 뒤의gauss.usda데이터 오프셋이 2,314,651,035 > 2^31(2,147,483,648). USD의 zip 리졸버가 32비트 오프셋을 넘겨 못 엶. - ETRI(nurec 364MB, offset 3.6e8)·WC(nurec 1.39GB, offset 1.39e9)는 2GiB 미만이라 정상.
해결: usdz를 디스크로 풀어 zip 리졸버를 우회.
# usdz → 폴더로 추출 (default.usda + gauss.usda + .nurec)
python3 -c "import zipfile; zipfile.ZipFile('output/USDZ_NOEUN/noeun_station_nurec.usdz').extractall('output/USDZ_NOEUN/noeun_station_nurec')"
# _nurec_mesh.usda 의 GS 참조를 usdz → 추출된 default.usda 로 변경
# @./noeun_station_nurec.usdz@ → @./noeun_station_nurec/default.usda@디스크에서는 default.usda→gauss.usda→.nurec 상대참조가 zip 오프셋 없이 정상 해석됨(TM.md 7절 "USDZ보다 USDA 직접 참조가 안정적"과 동일 교훈).
결과: NuRec Volume/OmniNuRecFieldAsset 정상 구성 → Isaac에서 GS 렌더 성공(사용자 확인). WC는 2GiB 미만이라 usdz 그대로 정상.
→ 규칙: 변환 후 .nurec이 2 GiB를 넘으면 usdz를 디스크로 풀어 default.usda를 참조한다.
이유: 평탄화 바닥 vs 울퉁불퉁 원본 메쉬 주행 비교 요청.
작업: --floor-shape full 추가 → 바닥/장애물 분리 없이 전체 스캔 메쉬(=VisualMesh와 동일 형상)를 단일 collider(approx=none, invisible)로 저작. 평탄 버전을 안 덮도록 _meshfloor_ 접미사로 분리 저장(_meshfloor_collision.usdc/_meshfloor_robot.usda). teleop/sensor_drive의 _robot.usda→_collision.usdc 유도 규칙과 호환되는 이름.
결과: ETRI1 생성 완료(/Colliders/FullMesh 47,065 faces). 두 환경을 --env로 바꿔가며 주행/LiDAR 비교 가능:
./run-sensor-drive.sh --index 1 # 평탄
./run-sensor-drive.sh --env output/USDZ_ETRI1/260521_ERTI_1_meshfloor_robot.usda # 원본 메쉬증상: run-sensor-drive.sh --env output/...(상대경로) 실행 시 환경 없음: /isaac-sim/output/... 에러.
원인: --env가 os.path.abspath()로 컨테이너 cwd(/isaac-sim) 기준으로 풀림. (--index는 이미 스크립트 위치 앵커라 정상)
작업: sensor_drive.py·teleop_test.py 둘 다 --env 상대경로를 스크립트 디렉토리 기준으로 앵커하도록 수정.
결과: cwd와 무관하게 상대경로 --env 동작.
output/USDZ_NOEUN/
├── noeun_station_nurec_mesh.usda ← Isaac에서 열 파일 (GS 참조 = 추출 폴더)
├── noeun_station_nurec/ ← usdz 추출본 (2GiB 초과 우회)
│ ├── default.usda / gauss.usda / noeun_station_nurec.nurec(2.3G)
├── noeun_station_nurec.usdz ← 원본 usdz (2.2G, 참조엔 미사용)
└── noeun_station_mesh.obj
output/USDZ_WC/
├── world_cup_stadium_station_nurec_mesh.usda ← Isaac에서 열 파일 (usdz 직접 참조 OK)
├── world_cup_stadium_station_nurec.usdz ← 1.3G (2GiB 미만이라 정상)
└── world_cup_stadium_station_mesh.obj
⚠️ .nurec/.usdz는 수 GB 대용량 → git 커밋 제외(코드/문서/스크립트만 커밋).