원본 Geometry를 건드리지 않고 운영 데이터를 쌓는 법
DAY 3에서 Reference, Payload, Variant로 도시를 조립했다. DAY 4에서는 Geometry·Material·Physics·Operations를 서로 다른 Layer로 분리하고, Edit Target과 opinion strength를 이용해 산업용 디지털 트윈에 가까운 구조로 바꾼다.
원본 Geometry 파일을 수정하지 않고 operations.usda에서 센서 속성과 운영 상태를 override하고, root layer가 geometry·environment·operations를 올바른 강도 순서로 합성하는지 Python으로 검증한다.
1. 30초 핵심 정리
| 구분 | 책임 | 변경 주기 |
|---|---|---|
| geometry.usda | Mesh·Xform·공간 구조 | 낮음 |
| environment.usda | 조명·날씨·환경 설정 | 중간 |
| operations.usda | 센서값·상태·운영 override | 높음 |
| city.usda | Layer 순서와 Stage 진입점 | 낮음 |
2. 왜 한 파일에 전부 넣으면 안 되는가?
작은 예제에서는 geometry와 센서값을 한 파일에 저장해도 동작한다. 하지만 실제 디지털 트윈에서는 CAD 변환 작업, 환경 연출, 물리 설정, IoT 갱신이 서로 다른 주기와 담당자를 가진다. 모든 의견을 한 Layer에 넣으면 센서값 하나를 바꿀 때도 무거운 자산 파일을 수정하게 되고, 충돌 원인과 책임 범위를 추적하기 어려워진다.
- 협업: 각 팀이 독립 Layer에서 작업한다.
- 안전: 원본 자산을 읽기 전용으로 유지한다.
- 성능: 무거운 geometry와 가벼운 운영 데이터를 분리한다.
- 진단: 어떤 Layer가 최종 값을 이겼는지 추적한다.
- 재사용: 같은 geometry를 여러 운영 시나리오에 사용한다.
3. DAY 4 파일 구조
smart-city-twin/
├── city.usda
├── assets/
│ ├── building.usda
│ ├── traffic_light.usda
│ └── sensor.usda
├── payloads/
│ └── building_geometry.usdc
├── layers/
│ ├── geometry.usda
│ ├── environment.usda
│ └── operations.usda
└── scripts/
├── create_layers.py
├── author_operations.py
└── inspect_layers.py
city.usda는 모든 데이터를 직접 소유하지 않는다. 어떤 Layer를
어떤 순서로 합성할지 선언하는 얇은 진입점 역할만 맡는다.
4. Root Layer와 Sublayer Stack 만들기
from pxr import Usd, Sdf
stage = Usd.Stage.CreateNew("city.usda")
root = stage.GetRootLayer()
# 앞쪽 항목이 더 강한 Layer다.
root.subLayerPaths = [
"./layers/operations.usda",
"./layers/environment.usda",
"./layers/geometry.usda",
]
stage.DefinePrim("/World", "Xform")
stage.SetDefaultPrim(stage.GetPrimAtPath("/World"))
root.Save()
5. Geometry Layer: 안정적인 기본 구조
from pxr import Usd, UsdGeom, Gf, Sdf
stage = Usd.Stage.CreateNew("layers/geometry.usda")
world = UsdGeom.Xform.Define(stage, "/World")
building = UsdGeom.Xform.Define(
stage, "/World/Buildings/Building_01"
)
building.AddTranslateOp().Set(Gf.Vec3d(0, 0, 0))
building.GetPrim().CreateAttribute(
"assetId", Sdf.ValueTypeNames.String
).Set("BLDG-001")
sensor = UsdGeom.Xform.Define(
stage, "/World/Sensors/Sensor_01"
)
sensor.AddTranslateOp().Set(Gf.Vec3d(4, 2, 1.5))
stage.SetDefaultPrim(world.GetPrim())
stage.GetRootLayer().Save()
Geometry Layer에는 센서의 위치와 식별 가능한 Prim path까지만 둔다. 실시간
값이나 경고 상태는 넣지 않는다. /World/Sensors/Sensor_01처럼
downstream Layer가 의존할 public path는 쉽게 바꾸지 않는다.
6. Operations Layer: over로 비파괴 운영 의견 작성
from pxr import Usd, Sdf
stage = Usd.Stage.CreateNew("layers/operations.usda")
sensor = stage.OverridePrim(
"/World/Sensors/Sensor_01"
)
sensor.CreateAttribute(
"sensorId", Sdf.ValueTypeNames.String
).Set("TEMP-001")
sensor.CreateAttribute(
"sensorType", Sdf.ValueTypeNames.Token
).Set("temperature")
sensor.CreateAttribute(
"value", Sdf.ValueTypeNames.Float
).Set(23.8)
sensor.CreateAttribute(
"unit", Sdf.ValueTypeNames.Token
).Set("celsius")
sensor.CreateAttribute(
"status", Sdf.ValueTypeNames.Token
).Set("normal")
sensor.CreateAttribute(
"lastUpdated", Sdf.ValueTypeNames.String
).Set("2026-08-16T09:00:00+09:00")
stage.GetRootLayer().Save()
OverridePrim()은 새 geometry를 복제하지 않고 같은 Prim path에
더 강한 의견만 기록한다. geometry Layer가 빠지면 이 over만으로는 일반
Traverse에서 완전한 정의 Prim처럼 취급되지 않는다는 점도 기억해야 한다.
7. Edit Target을 명시적으로 바꾸는 방법
from pxr import Usd, Sdf
stage = Usd.Stage.Open("city.usda")
ops = Sdf.Layer.FindOrOpen("layers/operations.usda")
if not ops:
raise RuntimeError("operations.usda를 열 수 없습니다.")
stage.SetEditTarget(ops)
sensor = stage.GetPrimAtPath(
"/World/Sensors/Sensor_01"
)
sensor.GetAttribute("value").Set(31.2)
sensor.GetAttribute("status").Set("warning")
ops.Save()
stage.GetEditTarget().GetLayer().identifier를 출력한다.
8. 최종 Stage와 Layer Stack 검증
from pxr import Usd
stage = Usd.Stage.Open("city.usda")
sensor = stage.GetPrimAtPath(
"/World/Sensors/Sensor_01"
)
assert sensor.IsValid()
assert sensor.GetAttribute("sensorId").Get() == "TEMP-001"
assert sensor.GetAttribute("value").Get() == 31.2
assert sensor.GetAttribute("status").Get() == "warning"
print("Edit Target:", stage.GetEditTarget().GetLayer().identifier)
print("Layer Stack:")
for layer in stage.GetLayerStack():
print(" -", layer.identifier)
errors = stage.GetCompositionErrors()
if errors:
for error in errors:
print("Composition Error:", error)
else:
print("Composition OK")
검증은 파일 존재 여부만 보는 것이 아니다. Layer 순서, 최종 속성값, public Prim path, composition error, 단위와 좌표계를 함께 확인해야 한다.
9. 실험: 강도 순서를 뒤집어 보기
-
operations.usda에서status = warning을 기록한다. -
environment.usda에도 같은 path에status = maintenance를 기록한다. - operations가 더 강할 때 최종값이 warning인지 확인한다.
- Sublayer 순서를 바꾼 뒤 maintenance로 바뀌는지 확인한다.
- Layer 하나를 mute하고 어느 의견이 남는지 확인한다.
이 실험을 직접 해보면 “strongest opinion wins”를 암기 문장이 아니라 관찰 가능한 결과로 설명할 수 있다.
10. 저장, Export, Flatten의 차이
- Save: 각 Layer의 구조와 composition arc를 유지한 채 변경된 Layer를 저장한다.
- Export: Stage 또는 Layer를 다른 파일로 내보낼 때 사용한다.
- Flatten: 합성 결과를 하나의 Layer로 굳힌다. 디버깅과 전달에는 유용하지만 원래의 모듈성과 편집 책임을 잃을 수 있다.
DAY 4의 목표는 flatten된 결과물을 만드는 것이 아니라 Layer가 분리된 상태로도 최종 Stage가 정확하게 합성되는지 증명하는 것이다.
11. 처음부터 끝까지 따라 하는 단계별 실습
이제 개념을 실제 파일로 확인한다. 아래 실습은 각 단계마다 무엇을 만들고, 어떤 명령을 실행하며, 성공했을 때 무엇이 보여야 하는지를 구분했다. 한꺼번에 모든 스크립트를 실행하지 말고 체크포인트를 통과한 뒤 다음 단계로 이동한다.
실습 0단계: Python 환경 확인
목표: 현재 Python에서 OpenUSD의 pxr 모듈을 불러올 수
있는지 확인한다.
python -c "from pxr import Usd, Sdf, UsdGeom; print(Usd.GetVersion())"
성공 기준: 예외 없이 USD 버전 튜플이 출력된다.
ModuleNotFoundError: pxr가 발생하면 일반 Python이 아니라
OpenUSD가 설치된 환경이나 Omniverse의 Python을 사용해야 한다.
실습 1단계: 프로젝트 폴더 만들기
목표: Layer의 상대경로가 예측 가능하도록 작업 디렉터리를 고정한다.
mkdir -p smart-city-twin/layers
mkdir -p smart-city-twin/scripts
cd smart-city-twin
Windows PowerShell에서는
New-Item -ItemType Directory -Force layers, scripts를 사용할 수
있다. 이후 모든 Python 명령은 smart-city-twin 폴더에서
실행한다.
체크포인트: 현재 폴더에 빈 layers/와
scripts/가 보인다.
실습 2단계: geometry.usda 생성
목표: 도시의 안정적인 공간 구조와 public Prim path를 만든다.
scripts/create_geometry.py를 만들고 다음 코드를 저장한다.
from pxr import Usd, UsdGeom, Gf, Sdf
stage = Usd.Stage.CreateNew("layers/geometry.usda")
world = UsdGeom.Xform.Define(stage, "/World")
UsdGeom.Xform.Define(stage, "/World/Buildings")
building = UsdGeom.Xform.Define(
stage, "/World/Buildings/Building_01"
)
building.AddTranslateOp().Set(Gf.Vec3d(0, 0, 0))
building.GetPrim().CreateAttribute(
"assetId", Sdf.ValueTypeNames.String
).Set("BLDG-001")
UsdGeom.Xform.Define(stage, "/World/Sensors")
sensor = UsdGeom.Xform.Define(
stage, "/World/Sensors/Sensor_01"
)
sensor.AddTranslateOp().Set(Gf.Vec3d(4, 2, 1.5))
stage.SetDefaultPrim(world.GetPrim())
stage.GetRootLayer().Save()
print("created:", stage.GetRootLayer().realPath)
python scripts/create_geometry.py
성공 기준: layers/geometry.usda가 생성되고, 텍스트로
열었을 때 World/Buildings/Building_01과
World/Sensors/Sensor_01이 존재한다.
확인 질문: 센서의 위치는 geometry에 있지만 현재 온도는 왜 없는가? 위치는 공간 모델의 일부이고, 온도는 자주 바뀌는 운영 데이터이기 때문이다.
실습 3단계: environment.usda 생성
목표: geometry와 독립적인 환경 의견을 만든다. DAY 4에서는 복잡한 조명 대신 간단한 metadata와 환경 Prim만 사용한다.
from pxr import Usd, UsdGeom, Sdf
stage = Usd.Stage.CreateNew("layers/environment.usda")
environment = stage.OverridePrim("/World/Environment")
environment.CreateAttribute(
"weather", Sdf.ValueTypeNames.Token
).Set("clear")
environment.CreateAttribute(
"ambientTemperature", Sdf.ValueTypeNames.Float
).Set(21.0)
stage.GetRootLayer().Save()
print("created: layers/environment.usda")
이 코드를 scripts/create_environment.py로 저장하고 실행한다.
아직 geometry와 합성하지 않았기 때문에 environment.usda만 열면 완전한 도시가
보이지 않는 것이 정상이다.
실습 4단계: operations.usda 생성
목표: 기존 센서 path 위에 현재 운영값을 비파괴적으로 작성한다.
from pxr import Usd, Sdf
stage = Usd.Stage.CreateNew("layers/operations.usda")
sensor = stage.OverridePrim("/World/Sensors/Sensor_01")
sensor.CreateAttribute(
"sensorId", Sdf.ValueTypeNames.String
).Set("TEMP-001")
sensor.CreateAttribute(
"sensorType", Sdf.ValueTypeNames.Token
).Set("temperature")
sensor.CreateAttribute(
"value", Sdf.ValueTypeNames.Float
).Set(23.8)
sensor.CreateAttribute(
"unit", Sdf.ValueTypeNames.Token
).Set("celsius")
sensor.CreateAttribute(
"status", Sdf.ValueTypeNames.Token
).Set("normal")
sensor.CreateAttribute(
"lastUpdated", Sdf.ValueTypeNames.String
).Set("2026-08-16T09:00:00+09:00")
stage.GetRootLayer().Save()
print("created: layers/operations.usda")
체크포인트: USDA 텍스트에서 over "World" 아래에 센서
속성이 보인다. Mesh나 xformOp를 복사하지 않았는데도 sensor path는 geometry와
정확히 같아야 한다.
실습 5단계: city.usda로 세 Layer 합성
목표: 얇은 root layer를 만들고 강도 순서를 명시한다.
from pxr import Usd
stage = Usd.Stage.CreateNew("city.usda")
root = stage.GetRootLayer()
root.subLayerPaths = [
"./layers/operations.usda",
"./layers/environment.usda",
"./layers/geometry.usda",
]
world = stage.GetPrimAtPath("/World")
if not world:
raise RuntimeError("/World가 합성되지 않았습니다.")
stage.SetDefaultPrim(world)
root.Save()
print("created: city.usda")
실행 결과를 해석하는 법: city.usda 텍스트는 매우 짧지만
이 파일을 Stage로 열면 geometry의 Prim과 operations의 센서 속성이 함께
보인다. 파일 크기가 작다고 Stage 내용도 적은 것은 아니다.
실습 6단계: 합성 결과 검사
목표: 눈으로만 확인하지 않고 코드로 성공 조건을 검증한다.
from pxr import Usd
stage = Usd.Stage.Open("city.usda")
sensor = stage.GetPrimAtPath("/World/Sensors/Sensor_01")
print("valid:", sensor.IsValid())
print("sensorId:", sensor.GetAttribute("sensorId").Get())
print("value:", sensor.GetAttribute("value").Get())
print("status:", sensor.GetAttribute("status").Get())
print("
Layer Stack - strong to weak")
for index, layer in enumerate(stage.GetLayerStack()):
print(index, layer.identifier)
errors = stage.GetCompositionErrors()
print("
Composition errors:", len(errors))
for error in errors:
print(error)
예상 결과: valid는 True, sensorId는 TEMP-001, value는 23.8, status는 normal이다. Layer Stack에는 city, operations, environment, geometry가 순서대로 나타나고 composition error는 0이어야 한다.
실습 7단계: Edit Target으로 운영값 갱신
목표: 합성된 Stage를 보면서도 변경값은 operations.usda에만 저장한다.
from pxr import Usd, Sdf
stage = Usd.Stage.Open("city.usda")
ops = Sdf.Layer.FindOrOpen("layers/operations.usda")
if not ops:
raise RuntimeError("operations.usda를 열 수 없습니다.")
stage.SetEditTarget(ops)
print(
"Edit Target:",
stage.GetEditTarget().GetLayer().identifier
)
sensor = stage.GetPrimAtPath(
"/World/Sensors/Sensor_01"
)
sensor.GetAttribute("value").Set(31.2)
sensor.GetAttribute("status").Set("warning")
ops.Save()
print("operations updated")
스크립트를 실행한 뒤 city.usda와 geometry.usda의
수정 시각은 바뀌지 않고 operations.usda만 변경되는지 확인한다. 다시 검사
스크립트를 실행하면 value는 31.2, status는 warning이어야 한다.
실습 8단계: 강도 충돌을 의도적으로 만들기
목표: “strongest opinion wins”를 직접 관찰한다.
-
environment.usda의 같은 센서 path에
status = maintenance를 추가한다. - operations가 environment보다 강한 현재 순서에서 최종값이 warning인지 확인한다.
- city.usda의 Sublayer 순서를 environment, operations, geometry로 바꾼다.
- Stage를 다시 열어 최종값이 maintenance로 바뀌는지 확인한다.
- 실험 후 Sublayer 순서를 원래대로 되돌린다.
실습 9단계: 잘못된 Prim path 오류 재현
목표: API 호출은 성공했지만 의도한 Prim에 값이 붙지 않는 문제를 재현한다.
-
operations.usda에서
/World/Sensors/Sensor_01을/World/Sensor/Sensor_01로 일부러 잘못 작성한다. - 검사 스크립트에서 원래 센서의 sensorId가 None인지 확인한다.
-
Stage를 순회해
/World/Sensor와/World/Sensors가 따로 존재하는지 출력한다. - path를 수정하고 정상 결과로 복구한다.
이 단계는 “오류 메시지는 없는데 데이터가 보이지 않는다”는 Composition 문제를 진단하는 연습이다. USD에서는 path가 데이터 계약이므로 철자 하나의 차이도 다른 namespace를 만든다.
실습 10단계: 최종 산출물 점검
-
city.usda는 Sublayer 목록과 진입점만 가진 얇은 파일인가? -
geometry.usda에 value나 status 같은 운영값이 들어가지 않았는가? -
operations.usda에 Mesh나 변환 geometry가 복제되지 않았는가? - Stage를 새로 열었을 때 센서의 모든 속성이 합성되는가?
- Layer Stack이 강한 순서에서 약한 순서로 의도와 일치하는가?
- Composition Error가 0인가?
여기까지 통과하면 DAY 4 완료 기준인 Geometry와 Operations 분리, 비파괴 override, Edit Target, Layer 강도 검증을 코드와 결과로 설명할 수 있다.
12. 3시간 실습 일정
| 0:00-0:30 | Layer Stack, Edit Target, opinion strength 복습 |
| 0:30-1:10 | geometry·environment·operations 파일 생성 |
| 1:10-2:00 | 센서 over와 Edit Target 코드 구현 |
| 2:00-2:30 | 강도 순서·mute·깨진 path 실험 |
| 2:30-3:00 | 검증 로그와 면접 답변 기록 |
13. 면접 답변으로 압축하기
Q. Geometry와 Operations를 왜 분리합니까?
Geometry는 크고 변경 주기가 낮지만 운영 데이터는 가볍고 자주 바뀝니다. 별도 Layer로 분리하면 원본 자산을 보호하고 협업 충돌을 줄이며, 같은 geometry를 여러 운영 시나리오에서 재사용할 수 있습니다.
Q. Edit Target은 무엇입니까?
Stage에서 다음 편집 의견이 실제로 기록될 Layer를 지정합니다. 합성 결과를 보고 있는 Layer와 값을 쓰는 Layer는 다를 수 있으므로 저장 전에 항상 Edit Target을 확인합니다.
Q. 충돌하는 값은 어떻게 결정됩니까?
같은 속성에 여러 의견이 있으면 composition strength에 따라 가장 강한 의견이 이깁니다. 문제를 진단할 때 Layer Stack, Prim Stack, Edit Target과 composition error를 함께 확인합니다.
14. DAY 4 체크리스트
□ city.usda에 Sublayer 순서를 명시했다.
□ Operations에서 기존 Prim을 over했다.
□ Edit Target을 operations.usda로 전환했다.
□ 같은 속성의 충돌과 강도 순서를 실험했다.
□ Layer Stack과 composition error를 출력했다.
□ 원본 Geometry가 변경되지 않았음을 확인했다.
□ Save, Export, Flatten의 차이를 설명했다.
마무리
DAY 4의 핵심은 파일을 많이 만드는 것이 아니라 데이터의 책임과 변경 주기를 Layer 경계로 표현하는 것이다. Geometry는 안정적인 기반으로, Operations는 자주 바뀌는 강한 의견으로 관리한다.
DAY 5에서는 이 Operations Layer에 환경 센서 5개의 sensorId,
sensorType, value, unit,
status, lastUpdated를 추가하고 임계값에 따라
상태와 색상을 변경한다.
0 댓글