4.4 GLB와 3D Tiles 1.1 통합
왜 3D Tiles 1.1이 b3dm/i3dm/pnts를 폐기하고 GLB를 직접 채택했는지, 그리고 새 확장(EXT_mesh_features 등)이 어떻게 그 자리를 메우는지. 업데이트: 2026-05-14
핵심 요약
| 구분 | 내용 |
|---|---|
| 📖 정의 | 3D Tiles 1.1은 OGC 표준 (2022-12-17 승인). tile content로 GLB를 직접 사용하고, 메타데이터·인스턴싱은 glTF 확장으로 표준화한다. |
| 💡 핵심 | b3dm/i3dm/pnts → GLB + EXT_* 5종 확장으로 완전 대체. 결과: 표준 glTF 도구(validator, Blender, Three.js)를 그대로 활용 가능. |
| 🎯 대상 | 3D Tileset 1.1을 처음부터 생성하거나 1.0 자산을 마이그레이션해야 하는 개발자 |
| ⚠️ 주의 | float32 정밀도 한계 때문에 ECEF 좌표를 GLB 버텍스에 그대로 넣으면 안 된다 — tile.transform으로 중심을 빼고 vertex는 상대 좌표를 써야 한다. |
목차
- 왜 1.1은 GLB를 직접 쓰는가
- 레거시 컨테이너 → GLB + 확장 매핑
- EXT_mesh_features — Feature ID 부여
- EXT_structural_metadata — Property Table
- EXT_mesh_gpu_instancing + EXT_instance_features
- CESIUM_RTC와 좌표 정밀도 문제
- Y-up ↔ Z-up 좌표계 변환
- tileset.json과 tile content 연결
- 1.0 → 1.1 마이그레이션 절차
- 실전 GLB 작성 체크리스트
- 검증·디버깅 도구
왜 1.1은 GLB를 직접 쓰는가
3D Tiles 1.0은 자체 컨테이너 포맷(b3dm = Batched 3D Model, i3dm = Instanced 3D Model, pnts = Point Cloud)을 사용했다. 각각은 헤더 + 메타데이터 테이블 + 임베드된 GLB 구조였다.
[3D Tiles 1.0] tileset.json → tile.b3dm
├── 28B header
├── Feature Table (binary JSON)
├── Batch Table (binary JSON)
└── GLB (실제 모델)
문제는 명확했다:
- 표준 도구(glTF-Validator, Blender, Three.js)가 b3dm을 직접 못 읽음 → 매번 GLB만 추출해서 작업
- Feature Table / Batch Table이 자체 binary JSON 포맷 → 별도 파서 필요
- 결국 GLB 위에 또 다른 컨테이너가 올라간 중첩 구조 — 중복
3D Tiles 1.1은 이걸 모두 GLB 안으로 흡수했다:
[3D Tiles 1.1] tileset.json → tile.glb (직접)
└── glTF 확장 안에 모든 메타데이터
Cesium 블로그 인용: “It is not a coincidence that the bitstream layout of a .glb glTF has a striking similarity to a .b3dm, as their initial design occurred concurrently.”
— b3dm과 GLB는 원래 동시 설계됐기 때문에 자연스럽게 수렴 가능했다.
결과:
- 표준 도구 직접 호환 — glTF-Validator, Blender, Three.js, Babylon.js가 그대로 작동
- 단일 파서 — JSON.parse + 표준 glTF 로더만 있으면 됨
- 편집·디버깅 용이 — Blender로 열어서 수정 가능
레거시 컨테이너 → GLB + 확장 매핑
| 1.0 구조 | 1.1 대체 |
|---|---|
.b3dm (Batched 3D Model) | .glb + EXT_mesh_features + EXT_structural_metadata |
.i3dm (Instanced 3D Model) | .glb + EXT_mesh_gpu_instancing + EXT_instance_features |
.pnts (Point Cloud) | .glb (point primitive, primitive.mode = 0) + EXT_mesh_features |
.cmpt (Composite) | 하나의 .glb에 여러 node로 구성 |
| Feature Table | glTF의 일반 속성 (POSITION 등) + EXT_mesh_features |
| Batch Table | EXT_structural_metadata.propertyTable |
BATCHID attribute | _FEATURE_ID_0 attribute (또는 EXT_mesh_features.featureIds의 attribute) |
1.0의 b3dm/i3dm/pnts는 deprecated이지만 클라이언트는 여전히 로드할 수 있다 (하위 호환). 신규 자산은 1.1 형식으로 만들 것.
EXT_mesh_features — Feature ID 부여
목적: 메쉬의 각 버텍스(또는 텍셀)에 “이건 어떤 객체에 속하는지”를 식별하는 ID를 부여한다. 이게 있어야 픽킹, 하이라이트, 속성 조회가 가능하다.
핵심 필드
"primitives": [{
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"_FEATURE_ID_0": 2
},
"extensions": {
"EXT_mesh_features": {
"featureIds": [{
"featureCount": 10,
"attribute": 0,
"propertyTable": 0
}]
}
}
}]| 필드 | 설명 |
|---|---|
featureCount | 이 메쉬에 있는 고유 feature 개수 |
attribute | _FEATURE_ID_<n> attribute 번호 (예: 0이면 _FEATURE_ID_0) |
propertyTable | EXT_structural_metadata의 propertyTable 인덱스 |
texture (선택) | feature ID를 텍스처로 인코딩 (포토그래메트리에서 유용) |
Feature ID를 attribute로 인코딩
_FEATURE_ID_0은 일반 attribute처럼 accessor를 가진다:
"accessors": [{
"bufferView": 5,
"componentType": 5123, // USHORT
"type": "SCALAR",
"count": 24000
}]값은 0부터 featureCount - 1까지의 정수. 한 버텍스 = 한 feature ID.
Feature ID를 텍스처로 인코딩
3D 메쉬 위에 텍스처로 feature를 칠하는 방식 (예: 항공사진에서 건물별 마스크):
"EXT_mesh_features": {
"featureIds": [{
"featureCount": 50,
"propertyTable": 0,
"texture": {
"index": 3, // textures[3]
"channels": [0] // R 채널 = feature ID
}
}]
}EXT_structural_metadata — Property Table
목적: Feature ID → 속성 행을 매핑하는 데이터 테이블 (1.0의 Batch Table 대체).
구조
"extensions": {
"EXT_structural_metadata": {
"schema": {
"id": "buildingSchema",
"classes": {
"building": {
"properties": {
"name": { "type": "STRING" },
"height": { "type": "SCALAR", "componentType": "FLOAT32" },
"year": { "type": "SCALAR", "componentType": "UINT16" }
}
}
}
},
"propertyTables": [{
"name": "buildings",
"class": "building",
"count": 10,
"properties": {
"name": { "values": 8, "stringOffsets": 9 },
"height": { "values": 10 },
"year": { "values": 11 }
}
}]
}
}해석 흐름
flowchart TD A["메쉬 위 클릭 픽킹"] --> B["맞은 vertex의<br/>_FEATURE_ID_0 값 = 5"] B --> C["EXT_mesh_features.featureIds[0]<br/>.propertyTable = 0"] C --> D["propertyTables[0]<br/>= buildingSchema.building"] D --> E["row index = 5 행의 데이터"] E --> F["name='City Hall', height=45.2, year=1924"] F --> G["UI에 팝업 표시"]
속성 타입
type | componentType | 설명 |
|---|---|---|
SCALAR | INT8~UINT64·FLOAT32·FLOAT64 | 단일 숫자 |
VEC2, VEC3, VEC4 | 같음 | 벡터 |
MAT2, MAT3, MAT4 | FLOAT32 등 | 행렬 |
STRING | (없음) | UTF-8 문자열 |
BOOLEAN | (없음) | bool |
ENUM | 별도 enum 정의 | 열거형 |
가변 길이(STRING, ARRAY)는 별도의 offsets accessor가 추가로 필요하다 (위 예제의 stringOffsets).
EXT_mesh_gpu_instancing + EXT_instance_features
목적: 같은 메쉬를 수백~수만 개 위치에 그릴 때 (1.0의 i3dm 대체).
EXT_mesh_gpu_instancing
"nodes": [{
"mesh": 0,
"extensions": {
"EXT_mesh_gpu_instancing": {
"attributes": {
"TRANSLATION": 5,
"ROTATION": 6,
"SCALE": 7
}
}
}
}]- accessor 5: VEC3 FLOAT (각 인스턴스의 translation)
- accessor 6: VEC4 FLOAT (각 인스턴스의 rotation 쿼터니언) — 선택
- accessor 7: VEC3 FLOAT (각 인스턴스의 scale) — 선택
세 accessor의 count는 같아야 하고, 그 값이 곧 인스턴스 개수다.
EXT_instance_features
각 인스턴스에도 Feature ID와 속성을 부여:
"EXT_instance_features": {
"featureIds": [{
"featureCount": 500,
"attribute": 0,
"propertyTable": 0
}]
}여기서 attribute: 0은 _FEATURE_ID_0 instance attribute를 가리킨다 (mesh attribute와 별개).
규칙:
EXT_instance_features는 반드시EXT_mesh_gpu_instancing과 같이 사용해야 한다.
활용 예시
도시 시뮬레이션에서 가로등 1만 개:
- mesh 0 = 가로등 1개 형상 (1번만 정의)
- node 0 = mesh 0을 인스턴싱, TRANSLATION accessor에 1만 개 위치
_FEATURE_ID_0(instance) → propertyTable의 “lamp_id, install_year, wattage” 행
VRAM·드로우 콜이 1만 배 감소한다.
CESIUM_RTC와 좌표 정밀도 문제
문제
지구의 ECEF(Earth-Centered Earth-Fixed) 좌표는 한국 기준으로 대략 (-3,200,000, 4,100,000, 3,800,000) m. 이 값을 float32에 저장하면:
- float32의 가수부는 23비트 → 약 7자리 정밀도
- 4,000,000 m 부근의 float32 간격 = 약 0.5 m
즉 1 m 미만의 디테일을 표현할 수 없고, 카메라가 움직이면 지터링(jittering) 이 발생한다.
CESIUM_RTC 해결 방식
타일 단위로 중심점(center) 을 뺀 상대 좌표를 vertex에 저장한다. 중심점은 별도 double precision으로 보관:
"extensions": {
"CESIUM_RTC": {
"center": [3963452.134, 378953.421, 4928234.567]
}
}런타임 처리:
finalECEF = CESIUM_RTC.center + vertex_position
(double) (float32 → double 캐스트)
vertex는 타일 중심으로부터 ±수백 m 범위라서 float32 정밀도 충분.
3D Tiles 1.1 권장 방식
CESIUM_RTC는 1.0 시절 확장이고, 1.1에서는 tile.transform을 활용하는 방식이 권장된다:
{
"boundingVolume": { ... },
"geometricError": 50,
"transform": [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
3963452.134, 378953.421, 4928234.567, 1
],
"content": { "uri": "tile.glb" }
}여기서 tile.transform은 double precision으로 다뤄지고, GLB 안의 vertex는 타일 로컬 좌표(중심 기준 상대) — 결과적으로 같은 효과를 표준 메커니즘으로 달성.
CESIUM_RTC는 여전히 정의되어 있으나 신규 자산은
tile.transform방식을 우선 사용한다.
Y-up ↔ Z-up 좌표계 변환
| 포맷 | Up 축 | 단위 |
|---|---|---|
| glTF | +Y up | meter |
| 3D Tiles | +Z up | meter (ECEF/local) |
3D Tiles 클라이언트(CesiumJS 등)는 GLB 로드 시 자동으로 Y-up → Z-up 변환을 적용한다:
변환 행렬 (X축 +90° 회전, 행 우선 표기):
┌ 1 0 0 0 ┐
│ 0 0 -1 0 │
│ 0 1 0 0 │
└ 0 0 0 1 ┘
변환 적용 순서
flowchart TD A["GLB 버텍스 (모델 공간, Y-up)"] --> B["glTF node hierarchy 변환<br/>(부모-자식 transform 누적)"] B --> C["Y-up → Z-up 변환<br/>(클라이언트 자동)"] C --> D["tile.transform 적용<br/>(타일 로컬 → tileset 로컬)"] D --> E["tileset root transform 적용<br/>(tileset 로컬 → WGS84 ECEF)"] E --> F["카메라 view matrix 적용 → 화면"]
⚠️ 흔한 실수: GLB 생성 시 Blender에서 Z-up으로 export하고 “내가 직접 변환했으니 클라이언트 자동변환 꺼라”고 하는 경우. CesiumJS는 자동변환을 끄는 옵션이 일반적으로 없다 — Blender는 반드시 Y-up으로 export한다 (glTF export 기본 옵션).
tileset.json과 tile content 연결
3D Tiles의 매니페스트 구조. tile content가 GLB라는 것만 명시하면 된다:
{
"asset": { "version": "1.1" },
"geometricError": 500,
"root": {
"boundingVolume": {
"region": [-1.3197, 0.6987, -1.3196, 0.6988, 0, 100]
},
"geometricError": 100,
"refine": "REPLACE",
"transform": [
0.96, 0, 0, 0,
0, 0.96, 0, 0,
0, 0, 0.96, 0,
3963452.134, 378953.421, 4928234.567, 1
],
"content": { "uri": "tiles/root.glb" },
"children": [
{
"boundingVolume": { },
"geometricError": 20,
"content": { "uri": "tiles/child_0.glb" }
}
]
}
}| 필드 | 설명 |
|---|---|
asset.version | "1.1" (문자열) — 1.0이면 b3dm 호환 모드 |
geometricError | LOD 전환 임계값 (미터). 작을수록 디테일 ↑ |
refine | ADD(누적) 또는 REPLACE(교체) |
transform | 타일 로컬 → 부모 좌표 변환 (열 우선 16-float, double) |
content.uri | .glb 파일 경로 (1.1에서 직접 GLB 가리킴) |
boundingVolume | box / region / sphere 중 하나 |
1.0 → 1.1 마이그레이션 절차
CesiumGS의 3d-tiles-tools CLI로 자동 변환 가능:
npm install -g 3d-tiles-tools
# 1.0 b3dm/i3dm/pnts → 1.1 GLB 변환
3d-tiles-tools upgrade \
--input ./oldTileset/tileset.json \
--output ./newTileset/tileset.json \
--targetVersion 1.1이 도구가 자동으로 수행하는 작업:
- tileset.json의
asset.version을"1.1"로 변경 .b3dm을 풀어서 embedded GLB 추출- b3dm의 Feature Table → GLB의
EXT_mesh_features로 이동 - b3dm의 Batch Table → GLB의
EXT_structural_metadatapropertyTable로 이동 BATCHIDattribute →_FEATURE_ID_0attribute로 이름 변경.i3dm을 풀어서EXT_mesh_gpu_instancing+EXT_instance_features로 변환.pnts를 GLB point primitive(mode=0)로 변환- content.uri를
.b3dm→.glb로 갱신
실전 GLB 작성 체크리스트
3D Tileset 1.1용 GLB를 만들 때 반드시 확인할 항목:
필수 (없으면 깨짐)
-
asset.version = "2.0", generator 명시 - 모든
POSITIONaccessor에min/max채움 (Cesium 컬링 박스용) - vertex는 타일 로컬 좌표 (중심 기준 상대 좌표) — 절대 ECEF 직접 금지
- Y-up·right-handed·미터 (glTF 표준)
-
extensionsRequired에 사용한 확장 명시 - sampler의
minFilter를 명시 (보통9987LINEAR_MIPMAP_LINEAR) → 상세
권장 (성능·품질)
- 텍스처를 KTX2 (Basis Universal) 포맷으로 압축 —
KHR_texture_basisu사용 - 지오메트리 압축:
KHR_draco_mesh_compression또는EXT_meshopt_compression - 정수 attribute 활용:
KHR_mesh_quantization - 픽킹·메타데이터 필요 시
EXT_mesh_features+EXT_structural_metadata부여 - 인스턴스 데이터는
EXT_mesh_gpu_instancing으로 - 포토그래메트리(항공사진 등)는
KHR_materials_unlit적용 → 조명 계산 생략으로 사실감 유지
검증
- glTF-Validator 통과 (오류 0건)
- 3d-tiles-validator 통과
- CesiumJS Sandcastle에서 실제 로드해보고 jittering 없는지 확인
검증·디버깅 도구
| 도구 | 용도 | 명령·링크 |
|---|---|---|
| glTF-Validator (CLI) | glTF 2.0 스펙 준수 검증 | npx gltf-validator model.glb |
| glTF-Validator (Web) | 드래그&드롭 검증 | github.khronos.org/glTF-Validator |
| 3d-tiles-validator | 3D Tiles 1.1 + 확장 검증 | npx 3d-tiles-validator tileset.json |
| 3d-tiles-tools | 1.0→1.1 업그레이드, GLB↔b3dm | npx 3d-tiles-tools upgrade ... |
| gltf-transform | KTX2 압축, Draco 압축, 최적화 | gltf-transform optimize in.glb out.glb |
| VS Code glTF Tools | JSON 자동완성, GLB↔glTF 변환, 3D 프리뷰 | Marketplace |
| CesiumJS Sandcastle | 타일셋 실시간 렌더링·디버깅 | sandcastle.cesium.com |
| glTF Sample Viewer | 머티리얼·확장 동작 확인 | github.com/KhronosGroup/glTF-Sample-Viewer |
| Blender (glTF I/O) | GLB export/import + 시각적 검사 | Blender 3.x 기본 내장 |
자주 만나는 검증 에러
| 에러 메시지 | 원인 | 해결 |
|---|---|---|
ACCESSOR_MIN_MISMATCH | accessor.min/max 값이 실제 데이터와 다름 | 정확히 계산해서 채우기 |
BUFFER_VIEW_TARGET_MISSING | bufferView.target 누락 | ARRAY_BUFFER(34962) 또는 ELEMENT_ARRAY_BUFFER(34963) 명시 |
INVALID_INDEX | 배열 인덱스가 범위 밖 | nodes/meshes 등 배열 길이 확인 |
UNSUPPORTED_EXTENSION | extensionsRequired의 확장 미지원 | 로더에 plugin 등록 (DRACOLoader, KTX2Loader 등) |
IMAGE_MIME_TYPE_INVALID | image.mimeType이 PNG/JPEG가 아님 | KTX2면 KHR_texture_basisu 명시 |
문서 탐색
| 이전 | 다음 |
|---|---|
| 4.3 GLB 도구와 실전 예제 | — |
참고 자료
- 3D Tiles 1.1 OGC Spec (공식 표준) — 2022-12-17 승인
- CesiumGS/3d-tiles GitHub
- Cesium 블로그 — Introducing 3D Tiles Next
- EXT_mesh_features (Khronos)
- EXT_structural_metadata (Khronos)
- EXT_mesh_gpu_instancing (Khronos)
- CESIUM_RTC README
- 3d-tiles-tools (CesiumGS)
- 3d-tiles-validator (CesiumGS)
- 4.1 GLB란 무엇인가
- 4.2 GLB 구조와 용어
- 4.3 GLB 도구와 실전 예제