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.1은 GLB를 직접 쓰는가
  2. 레거시 컨테이너 → GLB + 확장 매핑
  3. EXT_mesh_features — Feature ID 부여
  4. EXT_structural_metadata — Property Table
  5. EXT_mesh_gpu_instancing + EXT_instance_features
  6. CESIUM_RTC와 좌표 정밀도 문제
  7. Y-up ↔ Z-up 좌표계 변환
  8. tileset.json과 tile content 연결
  9. 1.0 → 1.1 마이그레이션 절차
  10. 실전 GLB 작성 체크리스트
  11. 검증·디버깅 도구

왜 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 (실제 모델)

문제는 명확했다:

  1. 표준 도구(glTF-Validator, Blender, Three.js)가 b3dm을 직접 못 읽음 → 매번 GLB만 추출해서 작업
  2. Feature Table / Batch Table이 자체 binary JSON 포맷 → 별도 파서 필요
  3. 결국 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 TableglTF의 일반 속성 (POSITION 등) + EXT_mesh_features
Batch TableEXT_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)
propertyTableEXT_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에 팝업 표시"]

속성 타입

typecomponentType설명
SCALARINT8~UINT64·FLOAT32·FLOAT64단일 숫자
VEC2, VEC3, VEC4같음벡터
MAT2, MAT3, MAT4FLOAT32행렬
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.transformdouble precision으로 다뤄지고, GLB 안의 vertex는 타일 로컬 좌표(중심 기준 상대) — 결과적으로 같은 효과를 표준 메커니즘으로 달성.

CESIUM_RTC는 여전히 정의되어 있으나 신규 자산은 tile.transform 방식을 우선 사용한다.


Y-up ↔ Z-up 좌표계 변환

포맷Up 축단위
glTF+Y upmeter
3D Tiles+Z upmeter (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 호환 모드
geometricErrorLOD 전환 임계값 (미터). 작을수록 디테일 ↑
refineADD(누적) 또는 REPLACE(교체)
transform타일 로컬 → 부모 좌표 변환 (열 우선 16-float, double)
content.uri.glb 파일 경로 (1.1에서 직접 GLB 가리킴)
boundingVolumebox / 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

이 도구가 자동으로 수행하는 작업:

  1. tileset.json의 asset.version"1.1"로 변경
  2. .b3dm을 풀어서 embedded GLB 추출
  3. b3dm의 Feature Table → GLB의 EXT_mesh_features로 이동
  4. b3dm의 Batch Table → GLB의 EXT_structural_metadata propertyTable로 이동
  5. BATCHID attribute → _FEATURE_ID_0 attribute로 이름 변경
  6. .i3dm을 풀어서 EXT_mesh_gpu_instancing + EXT_instance_features로 변환
  7. .pnts를 GLB point primitive(mode=0)로 변환
  8. content.uri를 .b3dm.glb로 갱신

실전 GLB 작성 체크리스트

3D Tileset 1.1용 GLB를 만들 때 반드시 확인할 항목:

필수 (없으면 깨짐)

  • asset.version = "2.0", generator 명시
  • 모든 POSITION accessor에 min/max 채움 (Cesium 컬링 박스용)
  • vertex는 타일 로컬 좌표 (중심 기준 상대 좌표) — 절대 ECEF 직접 금지
  • Y-up·right-handed·미터 (glTF 표준)
  • extensionsRequired에 사용한 확장 명시
  • sampler의 minFilter를 명시 (보통 9987 LINEAR_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 (CLI)glTF 2.0 스펙 준수 검증npx gltf-validator model.glb
glTF-Validator (Web)드래그&드롭 검증github.khronos.org/glTF-Validator
3d-tiles-validator3D Tiles 1.1 + 확장 검증npx 3d-tiles-validator tileset.json
3d-tiles-tools1.0→1.1 업그레이드, GLB↔b3dmnpx 3d-tiles-tools upgrade ...
gltf-transformKTX2 압축, Draco 압축, 최적화gltf-transform optimize in.glb out.glb
VS Code glTF ToolsJSON 자동완성, 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_MISMATCHaccessor.min/max 값이 실제 데이터와 다름정확히 계산해서 채우기
BUFFER_VIEW_TARGET_MISSINGbufferView.target 누락ARRAY_BUFFER(34962) 또는 ELEMENT_ARRAY_BUFFER(34963) 명시
INVALID_INDEX배열 인덱스가 범위 밖nodes/meshes 등 배열 길이 확인
UNSUPPORTED_EXTENSIONextensionsRequired의 확장 미지원로더에 plugin 등록 (DRACOLoader, KTX2Loader 등)
IMAGE_MIME_TYPE_INVALIDimage.mimeType이 PNG/JPEG가 아님KTX2면 KHR_texture_basisu 명시

문서 탐색


참고 자료