Python으로 첫 OpenUSD 도시 Stage 만들기
Stage 생성부터 Prim 계층·Transform·센서 Attribute·검증까지 한 번에 실습한다
면접 준비 둘째 날의 목표는 개념을 더 외우는 것이 아니다. Python 코드 한 번으로 열어볼 수 있는 도시 Scene Graph를 생성하고, 생성된 USDA 파일을 직접 읽으며 코드와 장면 구조의 대응 관계를 설명하는 것이다.
Python으로 새 Stage를 만들고,
/World/City 아래에 지면·건물·신호등·센서를 배치한다. 파일을 저장한 뒤 Prim Path와 Attribute를 다시 읽어 검증한다.1. 오늘 만들 결과
/World
└── /City
├── /Ground
├── /Buildings
│ ├── /Building_A
│ └── /Building_B
├── /TrafficLights
│ ├── /TrafficLight_01
│ └── /TrafficLight_02
└── /Sensors
├── /Sensor_01
└── /Sensor_02
이 구조에서 World와 그룹 Prim은 Xform, 지면과 건물은 Cube, 센서는 Sphere로 만든다. 실제 도시 모델을 만드는 것이 아니라 앞으로 Reference, Payload, Operational Layer를 적용할 작은 골격을 만드는 실습이다.
2. Python 환경 확인
OpenUSD Python binding이 준비된 터미널에서 다음 명령을 실행한다.
python -c "from pxr import Usd, UsdGeom, Gf; print('OpenUSD Python OK')"
ModuleNotFoundError: No module named 'pxr'가 나오면 현재 Python이 OpenUSD binding을 찾지 못한다는 뜻이다. OpenUSD 공식 튜토리얼은 OpenUSD를 빌드한 Python과 맞는 인터프리터를 사용하고, 설치 위치의 Python module과 실행 도구 경로를 설정하도록 안내한다.
OpenUSD 공식 튜토리얼 환경 설정 · 공식 Hello World Stage 생성 예제
오늘 실습은 RTX GPU나 Omniverse가 필요 없다. Intel N100과 내장 그래픽 환경에서도 Python으로 Stage와 USDA 파일을 만드는 작업은 가능하다. 3D Viewport보다 코드·파일 구조·검증에 집중한다.
3. Stage를 만드는 최소 코드
from pxr import Usd, UsdGeom
stage = Usd.Stage.CreateNew("hello_city.usda")
world = UsdGeom.Xform.Define(stage, "/World")
city = UsdGeom.Xform.Define(stage, "/World/City")
stage.SetDefaultPrim(world.GetPrim())
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)
UsdGeom.SetStageMetersPerUnit(stage, 1.0)
stage.GetRootLayer().Save()
Usd.Stage.CreateNew()는 새 Stage와 Root Layer를 생성한다. UsdGeom.Xform.Define()은 해당 경로에 Xform Schema Prim을 정의한다. 공식 Hello World도 같은 방식으로 Xform과 Sphere를 만들고 Root Layer를 저장한다.
| 코드 | 역할 |
|---|---|
CreateNew() | 새 USD 파일과 Stage 생성 |
Define() | Schema가 적용된 Prim 정의 |
SetDefaultPrim() | 외부에서 자산을 참조할 기본 Prim 지정 |
SetStageUpAxis() | 장면의 위쪽 축 지정 |
SetStageMetersPerUnit() | 한 단위가 실제 몇 미터인지 지정 |
Save() | 현재 Edit Target Layer 저장 |
4. 전체 도시 Stage 생성 코드
다음 내용을 build_day2_city.py로 저장한다.
# OpenUSD의 장면·지오메트리·수학·자료형 API를 불러온다.
from pxr import Usd, UsdGeom, Gf, Sdf
# 실행 결과를 사람이 읽을 수 있는 USDA 텍스트 파일로 저장한다.
OUTPUT = "day2_smart_city.usda"
# 새 Stage와 Root Layer를 생성한다.
stage = Usd.Stage.CreateNew(OUTPUT)
# Xform Prim을 그룹처럼 사용해 도시의 계층 구조를 만든다.
world = UsdGeom.Xform.Define(stage, "/World")
UsdGeom.Xform.Define(stage, "/World/City")
UsdGeom.Xform.Define(stage, "/World/City/Buildings")
UsdGeom.Xform.Define(stage, "/World/City/TrafficLights")
UsdGeom.Xform.Define(stage, "/World/City/Sensors")
# 다른 USD가 이 파일을 Reference할 때 대표가 될 Prim을 지정한다.
stage.SetDefaultPrim(world.GetPrim())
# Y축을 위쪽으로, 장면 1단위를 실제 1미터로 설정한다.
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.y)
UsdGeom.SetStageMetersPerUnit(stage, 1.0)
# 지면·건물·신호등에 사용할 Cube Prim 생성 함수다.
def make_cube(path, position, scale, color):
cube = UsdGeom.Cube.Define(stage, path)
cube.CreateSizeAttr(1.0)
# 이동과 크기는 Transform 연산으로 기록된다.
xform = UsdGeom.XformCommonAPI(cube)
xform.SetTranslate(Gf.Vec3d(*position))
xform.SetScale(Gf.Vec3f(*scale))
cube.CreateDisplayColorAttr([Gf.Vec3f(*color)])
return cube
# Sphere Prim을 만들고 센서 데이터를 Attribute로 기록한다.
def make_sensor(path, sensor_id, position, value):
sphere = UsdGeom.Sphere.Define(stage, path)
sphere.CreateRadiusAttr(0.3)
UsdGeom.XformCommonAPI(sphere).SetTranslate(Gf.Vec3d(*position))
sphere.CreateDisplayColorAttr([Gf.Vec3f(0.1, 0.9, 0.7)])
# 일반 UsdPrim 인터페이스에서 사용자 정의 Attribute를 만든다.
prim = sphere.GetPrim()
prim.CreateAttribute(
"sensorId", Sdf.ValueTypeNames.String
).Set(sensor_id)
prim.CreateAttribute(
"sensorType", Sdf.ValueTypeNames.String
).Set("temperature")
prim.CreateAttribute(
"value", Sdf.ValueTypeNames.Double
).Set(value)
prim.CreateAttribute(
"unit", Sdf.ValueTypeNames.String
).Set("celsius")
prim.CreateAttribute(
"status", Sdf.ValueTypeNames.String
).Set("Normal")
return sphere
# 지면과 두 개의 건물을 배치한다.
make_cube(
"/World/City/Ground",
(0, -0.1, 0), (20, 0.2, 20), (0.15, 0.18, 0.22)
)
building_a = make_cube(
"/World/City/Buildings/Building_A",
(-4, 3, 0), (4, 6, 4), (0.1, 0.35, 0.95)
)
building_b = make_cube(
"/World/City/Buildings/Building_B",
(4, 2, 0), (3, 4, 3), (0.25, 0.55, 0.9)
)
make_cube(
"/World/City/TrafficLights/TrafficLight_01",
(-2, 1, 5), (0.25, 2, 0.25), (0.2, 0.2, 0.2)
)
make_cube(
"/World/City/TrafficLights/TrafficLight_02",
(2, 1, -5), (0.25, 2, 0.25), (0.2, 0.2, 0.2)
)
# 각 건물 위에 온도 센서를 하나씩 배치한다.
sensor_01 = make_sensor(
"/World/City/Sensors/Sensor_01", "TEMP-001",
(-4, 6.5, 0), 24.5
)
sensor_02 = make_sensor(
"/World/City/Sensors/Sensor_02", "TEMP-002",
(4, 4.5, 0), 27.2
)
# 센서가 감시하는 건물은 다른 Prim을 가리키므로 Relationship이다.
sensor_01.GetPrim().CreateRelationship("monitors").SetTargets([
building_a.GetPath()
])
sensor_02.GetPrim().CreateRelationship("monitors").SetTargets([
building_b.GetPath()
])
# Root Layer에 기록한 내용을 실제 파일로 저장한다.
stage.GetRootLayer().Save()
print(f"Created: {OUTPUT}")
5. 코드를 이해하는 핵심 포인트
Schema 객체와 UsdPrim
UsdGeom.Cube.Define()은 Cube Schema 객체를 돌려준다. cube.GetPrim()을 호출하면 Scene Graph의 일반적인 Prim 인터페이스를 얻는다. Schema 객체는 크기·색상 같은 도메인별 기능을 편리하게 제공하고, UsdPrim은 Attribute·Relationship 등 공통 기능을 제공한다.
Define은 계층을 함께 만든다
/World/City/Buildings/Building_A처럼 깊은 경로를 정의하면 필요한 조상 Prim도 구성된다. 그러나 산업용 자산에서는 그룹의 의미를 명확히 하기 위해 Xform을 명시적으로 정의하는 편이 읽기 쉽다.
Transform은 값이 아니라 연산 순서다
Translate와 Scale은 xformOp로 기록되며 xformOpOrder가 적용 순서를 결정한다. USDA 파일을 열어 이 항목이 어떻게 생성됐는지 확인하는 것이 중요하다.
센서값과 대상 연결
온도 24.5는 센서가 가진 값이므로 Attribute다. 센서가 Building_A를 감시한다는 정보는 다른 Prim을 가리키므로 Relationship이다.
6. 실행하고 결과 확인하기
python build_day2_city.py
usdcat day2_smart_city.usda
usdview day2_smart_city.usda
usdview가 설치되지 않은 환경에서는 VS Code로 day2_smart_city.usda를 열어도 된다. USDA는 사람이 읽을 수 있는 텍스트이므로 Prim 계층과 속성을 직접 확인할 수 있다.
7. Python으로 다시 열어 검증하기
from pxr import Usd
stage = Usd.Stage.Open("day2_smart_city.usda")
required_paths = [
"/World",
"/World/City",
"/World/City/Buildings/Building_A",
"/World/City/Sensors/Sensor_01",
]
for path in required_paths:
prim = stage.GetPrimAtPath(path)
print(path, "OK" if prim.IsValid() else "MISSING")
sensor = stage.GetPrimAtPath(
"/World/City/Sensors/Sensor_01"
)
print("sensorId:", sensor.GetAttribute("sensorId").Get())
print("value:", sensor.GetAttribute("value").Get())
print("targets:",
sensor.GetRelationship("monitors").GetTargets())
파일이 저장됐다는 사실만으로 성공한 것이 아니다. 필요한 Prim이 유효한지, 센서 속성이 실제로 존재하는지, Relationship 대상이 올바른지 다시 읽어서 확인해야 한다.
8. 흔히 만나는 오류
| 증상 | 원인과 확인 방법 |
|---|---|
pxr import 실패 | Python 버전·PYTHONPATH·OpenUSD 설치 확인 |
| 빈 Stage가 보임 | Prim Path와 저장 대상 Layer 확인 |
| 객체 크기가 이상함 | metersPerUnit과 Scale 확인 |
| 객체 방향이 뒤집힘 | upAxis와 원본 자산 좌표계 확인 |
| 수정이 저장되지 않음 | 현재 Edit Target과 Save() 호출 확인 |
9. 면접에서 설명할 문장
왜 Python으로 Stage를 생성했는가?
반복 가능한 장면 생성과 자동 검증을 보여주기 위해서다. 수작업보다 자산 규칙을 코드로 재현하기 쉽고, 향후 CAD·GIS·IoT 데이터 파이프라인으로 확장할 수 있다.
왜 World를 defaultPrim으로 지정했는가?
다른 Stage가 이 자산을 Reference할 때 내부 경로를 매번 지정하지 않아도 대표 Prim을 찾을 수 있도록 하기 위해서다.
왜 센서값을 Attribute로 만들었는가?
온도는 Sensor Prim 자체의 상태값이기 때문이다. 센서가 감시하는 건물은 객체 간 연결이므로 Relationship으로 분리했다.
오늘 구현의 한계는 무엇인가?
모든 내용이 하나의 Root Layer에 들어 있다. 다음 단계에서는 자산을 별도 파일로 분리하고 Reference·Payload·Variant를 적용해 실제 제작 파이프라인에 가까운 구조로 개선한다.
10. DAY 2 체크리스트
from pxr import Usd성공day2_smart_city.usda생성/World/City계층 확인- 건물·신호등·센서 Prim 확인
- 센서 custom Attribute 확인
monitorsRelationship 확인- Stage를 다시 열어 필수 경로 검증
- 코드 선택 이유를 60초 안에 설명
다음 학습: Composition
DAY 3에서는 오늘 만든 하나의 파일을 자산과 운영 데이터로 분리한다. Sublayer, Reference, Payload, Variant가 각각 어떤 문제를 해결하는지 실제 파일 구조로 적용한다.
이 글은 NVIDIA Senior Omniverse Engineer 면접을 준비하며 작성한 개인 학습 기록입니다. 예제는 OpenUSD 공식 Python 튜토리얼과 API 구조를 바탕으로 스마트시티 상황에 맞게 재구성했습니다.

0 댓글