4.2 GLB 구조와 용어
GLB 바이너리 레이아웃, glTF JSON 컴포넌트, 데이터 3계층(buffer/bufferView/accessor), 용어 사전, 그리고 바이트가 GPU 픽셀로 변환되는 파이프라인. 업데이트: 2026-05-14
핵심 요약
| 구분 | 내용 |
|---|---|
| 📖 정의 | GLB = Header + JSON Chunk + Binary Chunk. JSON이 씬 그래프를 기술하고 BIN이 실제 바이트를 담는다. 모든 참조는 배열 인덱스(integer) 기반. |
| 💡 핵심 | buffer → bufferView → accessor 3단 계층을 거쳐 raw 바이트가 의미를 부여받는다. GPU 업로드 직전까지 중간 변환 없음. |
| 🎯 대상 | GLB를 생성·수정·검증·파싱하거나 렌더링 품질 문제를 분석하는 개발자 |
| ⚠️ 주의 | (bufferView.byteOffset + accessor.byteOffset) % sizeof(componentType) == 0 정렬 규칙을 어기면 GPU 업로드는 성공하지만 시각적으로 무작위 결과가 나온다. Validator로 사전 검증 필수. |
목차
- GLB 바이너리 레이아웃
- glTF JSON 최상위 컨테이너
- 씬 그래프 용어 (scene·node·mesh·primitive)
- 데이터 3계층 (buffer·bufferView·accessor)
- 머티리얼·텍스처 용어
- Sampler — 텍스처 필터링
- 애니메이션·스키닝 용어
- 확장(KHR/EXT) 용어
- 렌더링 파이프라인 7단계
- 디버깅 시 자주 보게 되는 함정
- JSON 최상위 객체 한눈에 보기
GLB 바이너리 레이아웃
GLB는 리틀 엔디언(Little-endian) 단일 바이너리 컨테이너다. 1개(JSON only) 또는 2개(JSON + BIN) 청크를 가진다.
12바이트 헤더
| 필드 | 오프셋 | 크기 | 값 |
|---|---|---|---|
| magic | 0 | 4B | 0x46546C67 (ASCII “glTF”) |
| version | 4 | 4B | 2 |
| length | 8 | 4B | 전체 파일 바이트 수 (헤더 포함) |
청크 구조
+0 [chunkLength: 4B uint32] ← chunkData의 바이트 수 (패딩 제외)
+4 [chunkType: 4B uint32] ← 청크 종류
+8 [chunkData: N bytes ] ← 실제 데이터
+8+N [padding: P bytes ] ← (4 - N%4) % 4 바이트 패딩
| 청크 | chunkType (hex) | 패딩 문자 | 순서·필수 여부 |
|---|---|---|---|
| JSON | 0x4E4F534A (ASCII “JSON”) | 0x20 (Space) | 첫 번째 · 필수 |
| BIN | 0x004E4942 (ASCII “BIN\0”) | 0x00 (Null) | 두 번째 · 선택 |
전체 레이아웃
┌──────────────────────────────────────────┐
│ Header (12 bytes) │
│ [magic:4B] [version:4B] [length:4B] │
├──────────────────────────────────────────┤
│ JSON Chunk Header (8 bytes) │
│ [chunkLength:4B] [0x4E4F534A:4B "JSON"] │
├──────────────────────────────────────────┤
│ JSON Chunk Data (UTF-8) + 0x20 padding │
│ { "asset": {"version":"2.0"}, ... } │
├──────────────────────────────────────────┤
│ BIN Chunk Header (8 bytes) │
│ [chunkLength:4B] [0x004E4942:4B "BIN\0"]│
├──────────────────────────────────────────┤
│ BIN Chunk Data + 0x00 padding │
│ [vertices][indices][images][...] │
└──────────────────────────────────────────┘
바이트 오프셋 계산 예시
JSON이 217바이트라면:
JSON 패딩 = ceil(217/4)*4 - 217 = 3 (0x20 × 3)
BIN 청크 시작 = 12 (header) + 8 (JSON header) + 220 = 240
GLB에 임베드된 buffer: JSON에서
buffers[0]에uri필드를 생략하면, 그 buffer는 자동으로 BIN 청크를 참조한다. 단,buffers[0]만 임베드 가능하다.
glTF JSON 최상위 컨테이너
glTF JSON 루트(root)에 정의되는 최상위 객체들이다. 대부분 배열이며 정수 인덱스로 상호 참조한다.
| 용어 | 설명 | 비고 |
|---|---|---|
| glTF | ”GL Transmission Format”. Khronos가 정의한 3D 자산 런타임 포맷. | ”3D 계의 JPEG” |
| GLB | glTF의 단일 바이너리 컨테이너. JSON·BIN·텍스처를 하나의 .glb 파일로 묶는다. | 미디어 타입 model/gltf-binary |
| asset | glTF 버전·생성기 정보를 담는 필수 객체. | {"version": "2.0"} 필수 |
| scene | 화면에 렌더링될 노드들의 집합. 여러 개 정의 가능. | 기본 씬은 scene 정수 인덱스 |
| scenes | 모든 씬 정의의 배열. | |
| nodes | 씬 그래프 노드 배열. 각 노드는 transform과 mesh/camera/skin 참조를 가진다. | |
| meshes | 메시 정의 배열. 각 메시는 primitive 배열을 가진다. | |
| accessors | bufferView를 typed array처럼 해석하는 메타데이터 배열. | |
| bufferViews | buffer의 한 구간을 가리키는 뷰 배열. | |
| buffers | 원시 바이트 덩어리 배열 (URI 또는 GLB의 BIN 청크). | |
| materials | PBR 머티리얼 배열. | |
| textures | image + sampler 조합 배열. | |
| images | 이미지 소스 배열 (URI/data URI 또는 bufferView 참조). | PNG/JPEG, KTX2(확장) |
| samplers | 텍스처 샘플링 파라미터 배열 (filter·wrap). | |
| skins | 스키닝(본 애니메이션) 정의 배열. | |
| animations | 애니메이션 클립 배열. | |
| cameras | 카메라 정의 배열 (perspective / orthographic). | |
| extensionsUsed | 파일이 사용하는 확장 이름 배열. | 정보용 |
| extensionsRequired | 파일이 반드시 필요로 하는 확장 이름 배열. | 로더가 지원 못 하면 거부 |
인덱스 참조 규칙: 예를 들어
node.mesh = 3은meshes[3]을 가리킨다. ID 문자열이 아닌 정수 인덱스만 쓴다 — O(1) 조회와 JSON 용량 최소화 목적.
씬 그래프 용어 (scene·node·mesh·primitive)
| 용어 | 설명 | 비고 |
|---|---|---|
| scene | 화면에 그릴 root node 인덱스들의 집합. { "nodes": [0, 3] }. | 카메라·라이트도 노드로 포함 가능 |
| node | 씬 그래프의 한 노드. transform(matrix 또는 TRS)과 선택적으로 mesh/camera/skin/light/children을 가진다. | DAG 금지(트리만 허용) |
| children | 자식 노드 인덱스 배열. 부모의 transform을 상속한다. | 한 노드는 한 부모만 |
| matrix | 4×4 변환 행렬. 열 우선(column-major) 16-float 배열. | TRS와 동시 사용 금지 |
| TRS | translation·rotation·scale 3개 필드. 합성 순서 M = T × R × S. | 권장 방식 |
| translation | 3-float [tx, ty, tz]. | |
| rotation | 단위 쿼터니언 [qx, qy, qz, qw] — w가 마지막이다. | 헷갈리기 쉬움 |
| scale | 3-float [sx, sy, sz]. | 음수도 가능(반전) |
| mesh | 같은 transform을 공유하는 primitive들의 묶음. 한 노드는 한 메시만 참조. | |
| primitive | GPU의 draw call 1개에 대응되는 단위. attributes + indices + material + mode. | 보통 머티리얼별로 나뉜다 |
| mode | GPU 토폴로지: POINTS(0), LINES(1), LINE_LOOP(2), LINE_STRIP(3), TRIANGLES(4), TRIANGLE_STRIP(5), TRIANGLE_FAN(6). | 기본값 = 4 (TRIANGLES) |
| attributes | primitive의 버텍스 속성 맵. POSITION, NORMAL, TEXCOORD_0 등 시맨틱 이름 → accessor 인덱스. | |
| indices | 인덱스 버퍼 accessor 인덱스. 없으면 non-indexed draw. | |
| morph target | primitive의 위치 변형 정보 (블렌드 셰이프). primitive.targets 배열. | 얼굴 표정 등 |
| weights | 모프 타깃 가중치 배열. mesh 또는 node 단위로 정의 가능. |
표준 attribute 시맨틱
| 시맨틱 | 타입 | 설명 | 비고 |
|---|---|---|---|
POSITION | FLOAT VEC3 | 버텍스 위치 (모델 공간) | 필수, accessor에 min/max 필수 |
NORMAL | FLOAT VEC3 | 단위 법선 벡터 | 정규화됨 |
TANGENT | FLOAT VEC4 | 탄젠트 + w(handedness ±1) | 노멀맵 사용 시 필요 |
TEXCOORD_0, TEXCOORD_1, … | FLOAT/UBYTE/USHORT VEC2 | UV 좌표. 원점은 좌상단 | normalized 가능 |
COLOR_0 | FLOAT/UBYTE/USHORT VEC3 또는 VEC4 | 버텍스 컬러 (선형 RGB/RGBA) | normalized 가능 |
JOINTS_0, JOINTS_1 | UBYTE/USHORT VEC4 | 영향 받는 관절 인덱스 | 스키닝용 |
WEIGHTS_0, WEIGHTS_1 | FLOAT/UBYTE/USHORT VEC4 | 각 관절의 가중치 (합 = 1.0) | normalized 가능 |
_FEATURE_ID_0, _BATCHID | INT 계열 | 커스텀 또는 확장 attribute | _ prefix 필수 |
node transform — matrix vs TRS
각 노드는 두 가지 방식 중 하나로 transform을 표기한다 (둘 다 정의 시 invalid).
matrix 방식 (열 우선 16-float):
{
"matrix": [
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
}TRS 방식 (권장):
{
"translation": [0, 1.5, 0],
"rotation": [0, 0, 0, 1],
"scale": [1, 1, 1]
}합성 순서: M = T × R × S (스케일 먼저, 그다음 회전, 마지막 이동).
⚠️ rotation의 quaternion 순서:
[qx, qy, qz, qw]— w가 맨 마지막이다. Three.js/CesiumJS도 같은 규약이지만, 다른 수학 라이브러리(예: Eigen)는 w가 앞에 오는 경우가 있다. 변환 시 주의.
부모-자식 transform 누적
자식 노드는 부모의 transform을 상속한다. 어떤 노드 N의 월드 행렬은:
M_world(N) = M_root × M_intermediate_1 × ... × M_parent(N) × M_local(N)
3D Tiles 1.1에서는 이 누적된 모델 공간 좌표 위에 다시 tile.transform과 tileset root transform이 곱해진다. → 상세
데이터 3계층 (buffer·bufferView·accessor)
glTF의 가장 중요한 추상화. 원시 바이트 → 의미 있는 typed array로 단계적으로 해석된다.
buffer (raw bytes)
└── bufferView (구간 + GPU 힌트)
└── accessor (자료형 + 카운트 + 통계)
| 용어 | 설명 | 비고 |
|---|---|---|
| buffer | 원시 바이트 덩어리. uri(외부 .bin 또는 data URI) 또는 GLB의 BIN 청크 참조(uri 생략). | byteLength 필수 |
| bufferView | buffer의 한 구간을 가리키는 뷰. byteOffset, byteLength, 선택적으로 byteStride·target. | GPU 업로드 단위 |
| byteOffset | buffer 시작에서의 오프셋(바이트). | 4바이트 정렬 권장 |
| byteLength | 뷰의 크기(바이트). | |
| byteStride | 한 요소의 stride(바이트). interleaved 버텍스 데이터에서 사용 (4~252). | bufferView 속성 |
| target | 34962 = ARRAY_BUFFER (버텍스 어트리뷰트) / 34963 = ELEMENT_ARRAY_BUFFER (인덱스). | GL hint |
| accessor | bufferView를 typed array처럼 해석하는 메타데이터. | ”이게 무슨 자료형이고 몇 개냐”를 알려준다 |
| componentType | 자료형 GL 상수 (아래 표 참조). | |
| type | 컴포넌트 묶음: SCALAR / VEC2 / VEC3 / VEC4 / MAT2 / MAT3 / MAT4. | |
| count | 요소의 개수 (바이트 수 아님). | |
| min / max | 각 컴포넌트별 최솟값/최댓값 배열. POSITION에 필수 (컬링 박스 계산용). | Cesium에서 결정적 |
| normalized | 정수 → 정규화된 float 변환 (UBYTE/USHORT/SBYTE/SSHORT만). UNSIGNED_INT에는 사용 금지. | |
| sparse | 대부분 0인 데이터를 효율 저장. count, indices, values 하위 객체. | 모프 타깃 등 |
componentType + type 조합표
| componentType | GL 상수 | C 타입 | 크기 | type 예 | TypedArray |
|---|---|---|---|---|---|
| 5120 | BYTE | int8_t | 1B | SCALAR | Int8Array |
| 5121 | UNSIGNED_BYTE | uint8_t | 1B | VEC4 (normalized) | Uint8Array |
| 5122 | SHORT | int16_t | 2B | SCALAR | Int16Array |
| 5123 | UNSIGNED_SHORT | uint16_t | 2B | SCALAR | Uint16Array |
| 5125 | UNSIGNED_INT | uint32_t | 4B | SCALAR | Uint32Array |
| 5126 | FLOAT | float | 4B | VEC3 (12B) | Float32Array |
정렬 규칙:
(bufferView.byteOffset + accessor.byteOffset) % sizeof(componentType) == 0이어야 한다. FLOAT accessor는 합이 4의 배수, USHORT는 2의 배수.
sparse accessor
bufferView를 생략하고 sparse 객체만 두면 “기본값은 0, 일부 인덱스만 값 있음”으로 해석된다. 모프 타깃처럼 대부분 0인 데이터에 사용.
{
"componentType": 5126, "type": "VEC3", "count": 1000,
"sparse": {
"count": 5,
"indices": { "bufferView": 3, "componentType": 5125 },
"values": { "bufferView": 4 }
}
}머티리얼·텍스처 용어
| 용어 | 설명 | 비고 |
|---|---|---|
| material | PBR 머티리얼. primitive에 인덱스로 연결. | |
| PBR | Physically Based Rendering. glTF 2.0은 Metallic-Roughness 워크플로우를 기본 채택. | |
| pbrMetallicRoughness | baseColor·metallic·roughness 필드를 묶은 객체. | |
| baseColorFactor | RGBA 4-float. 기본 색상 배율. | 선형 색공간 |
| baseColorTexture | 베이스 컬러 텍스처. | RGB=색상, A=알파, sRGB |
| metallicFactor / roughnessFactor | 스칼라 배율 (0~1). | 기본 1.0 |
| metallicRoughnessTexture | G=roughness, B=metallic 채널 패킹된 텍스처. | linear |
| normalTexture | 탄젠트 공간 노멀맵. scale 필드로 강도 조절. | linear |
| occlusionTexture | R=AO 텍스처. strength로 강도 조절. | linear |
| emissiveTexture / emissiveFactor | 자발광 텍스처 / 배율. | sRGB |
| alphaMode | OPAQUE (기본) / MASK (alphaCutoff로 on-off) / BLEND (반투명). | BLEND는 비용 ↑ |
| alphaCutoff | MASK 모드 임계값 (기본 0.5). | |
| doubleSided | true면 양면 렌더링. | 잎사귀·천 |
| texture | image + sampler 조합. | |
| image | 이미지 데이터. uri(외부/data URI) 또는 bufferView+mimeType. | PNG, JPEG, KTX2 |
| TextureInfo | 머티리얼에서 텍스처를 가리킬 때 쓰는 객체 { "index": n, "texCoord": 0 }. | texCoord는 사용할 TEXCOORD_n |
텍스처 채널 매핑 (반드시 지킬 것)
| 텍스처 | 채널 | 색공간 |
|---|---|---|
| baseColorTexture | RGB=색상, A=알파 | sRGB |
| metallicRoughnessTexture | G=roughness, B=metallic | linear |
| normalTexture | RGB=탄젠트 노멀 | linear |
| occlusionTexture | R=AO | linear |
| emissiveTexture | RGB=자발광 | sRGB |
sRGB 텍스처는 GPU에서 자동 감마 디코드 → 셰이더가 linear 값으로 계산.
Sampler — 텍스처 필터링
Sampler는 GPU가 텍스처를 읽는 방식을 정의한다. 텍스처 이미지 데이터와 별도로, “어떻게 보간/축소할 것인가” 를 지정한다.
⚠️ 핵심 주의:
minFilter와magFilter가null(미설정)이면 glTF 스펙에 기본값이 명시되어 있지 않다. WebGL 구현체마다 다른 기본값을 사용하며, CesiumJS 3D Tiles 경로에서는 Mipmap이 존재해도 무시된다. → 보정 코드와 분석
minFilter — 축소 필터 (카메라가 멀리 있을 때)
| 값 | 상수명 | 동작 | 권장 |
|---|---|---|---|
| 9728 | NEAREST | Mipmap 미사용, 최근접 텍셀 1개 | — |
| 9729 | LINEAR | Mipmap 미사용, 4개 텍셀 선형 보간 | — |
| 9984 | NEAREST_MIPMAP_NEAREST | 최근접 Mipmap 1개 → NEAREST | — |
| 9985 | LINEAR_MIPMAP_NEAREST | 최근접 Mipmap 1개 → LINEAR | — |
| 9986 | NEAREST_MIPMAP_LINEAR | 인접 Mipmap 2개 보간 → NEAREST | — |
| 9987 | LINEAR_MIPMAP_LINEAR | 인접 Mipmap 2개 보간 → LINEAR (Trilinear) | ✅ 최고 품질 |
magFilter — 확대 필터 (카메라가 매우 가까이 있을 때)
| 값 | 상수명 | 동작 |
|---|---|---|
| 9728 | NEAREST | 픽셀아트 느낌 (픽셀화) |
| 9729 | LINEAR | 부드러운 경계 (bilinear) ✅ |
magFilter에는 9728(NEAREST)과 9729(LINEAR)만 유효하다. Mipmap 관련 값(9984~9987)은minFilter에서만 사용 가능.
wrapS / wrapT — 래핑 모드
| 값 | 상수명 | 설명 |
|---|---|---|
| 33071 | CLAMP_TO_EDGE | 가장자리 색상으로 채움 |
| 33648 | MIRRORED_REPEAT | 거울 반전 반복 |
| 10497 | REPEAT | 그대로 반복 (기본값) |
권장 sampler 설정값
{
"samplers": [{
"minFilter": 9987,
"magFilter": 9729,
"wrapS": 10497,
"wrapT": 10497
}]
}애니메이션·스키닝 용어
| 용어 | 설명 | 비고 |
|---|---|---|
| animation | 시간 기반 키프레임 클립. channels + samplers. | |
| channel | 어떤 노드의 어떤 속성을 애니메이션 할지 지정. { "sampler": n, "target": { "node": m, "path": "..." } }. | |
| path | 애니메이션 대상 속성: translation / rotation / scale / weights. | |
| sampler (animation) | 시간(input accessor) → 값(output accessor) 매핑 + 보간법. | 텍스처 sampler와 별개 개념 |
| interpolation | LINEAR / STEP / CUBICSPLINE. | rotation의 LINEAR은 slerp |
| skin | 스키닝 정의. joints(노드 인덱스 배열), inverseBindMatrices(accessor), 선택적 skeleton. | |
| joint | 스키닝의 본(bone)에 해당하는 노드. | |
| inverseBindMatrix | 각 joint의 바인드 포즈 역행렬. 모델 공간 → joint 로컬 공간 변환. | MAT4 |
| skinning | JOINTS_0/WEIGHTS_0 attribute를 사용해 버텍스를 본에 따라 변형. |
스키닝 공식
JOINTS_0[v] = [j0, j1, j2, j3] (관절 인덱스)
WEIGHTS_0[v] = [w0, w1, w2, w3] (합 = 1.0)
jointMatrix[j] = globalTransform(nodes[skin.joints[j]]) × inverseBindMatrices[j]
skinMatrix = Σ WEIGHTS_0[v][i] × jointMatrix[JOINTS_0[v][i]] (i=0~3)
finalPos = skinMatrix × vec4(POSITION[v], 1.0)
확장(KHR/EXT) 용어
| 용어 | 설명 | 비고 |
|---|---|---|
| extension | 스펙에 추가 기능을 더하는 모듈. 객체 안 "extensions": { "EXT_xxx": {...} }로 삽입. | |
| KHR_ | Khronos 표준화 확장 (Ratified). | |
| EXT_ | 다중 벤더 확장 (community/multi-vendor). | |
벤더_ (예: CESIUM_) | 단일 벤더 확장. | |
| extensionsUsed | 파일이 사용하는 확장 이름 배열 (정보용). | |
| extensionsRequired | 파일이 반드시 필요로 하는 확장 — 미지원 로더는 거부해야 한다. | |
| KHR_draco_mesh_compression | Draco 지오메트리 압축 (손실, 최대 95% ↓). | 정점 압축 |
| KHR_texture_basisu | KTX2/Basis Universal 텍스처 압축. GPU 직접 사용. | VRAM 절약 |
| EXT_meshopt_compression | meshopt 압축 (bufferView 단위, ~1 GB/s 디코드). | 빠른 스트리밍 |
| KHR_materials_unlit | 조명 계산 무시, baseColor만 출력. | 포토그래메트리 필수 |
| KHR_materials_clearcoat | 클리어코트 레이어 (자동차 도장 등). | |
| KHR_materials_sheen | 천·벨벳 표현. | |
| KHR_materials_transmission / volume / ior | 유리·물 표현. | |
| KHR_materials_specular | 스페큘러 워크플로우 보강. | |
| KHR_materials_emissive_strength | emissive 밝기 1.0 이상 허용. | HDR 발광 |
| KHR_mesh_quantization | POSITION/NORMAL/UV를 정수형으로 저장 → 크기 1/2~1/4. | |
| KHR_texture_transform | UV offset/rotation/scale 변환. | 텍스처 아틀라스 |
| KHR_lights_punctual | 점광원 정의. |
3D Tiles 1.1 전용 확장(EXT_mesh_features 등)은 4.4 참조.
렌더링 파이프라인 7단계
GLB 바이트가 JSON으로, accessor로, GPU 버퍼로, 마지막으로 픽셀로 변환되는 흐름.
| # | 단계명 | 설명 | 입력 → 출력 |
|---|---|---|---|
| 1 | GLB 바이트 분해 | 헤더·청크 분리 | Uint8Array → { json: string, bin: ArrayBuffer } |
| 2 | JSON 파싱 | UTF-8 JSON 디코딩 | string → glTFRoot 객체 |
| 3 | buffer 확보 | BIN 청크 또는 URI 페치 | glTFRoot.buffers → ArrayBuffer[] |
| 4 | bufferView → accessor | 구간 + 자료형 해석 | ArrayBuffer + 메타 → TypedArrayView |
| 5 | primitive attribute 매핑 | 시맨틱 → GPU 버퍼 바인딩 | accessor → VertexBuffer, IndexBuffer |
| 6 | 머티리얼·텍스처 업로드 | image 디코드 + sampler 설정 | image bytes → GPU Texture |
| 7 | transform 누적 + draw | scene 순회 + 머티리얼 적용 | scene + 카메라 → 픽셀 |
sequenceDiagram autonumber participant U as User Code participant L as Loader<br/>(GLTFLoader 등) participant P as Parser participant G as GPU/WebGL U->>L: fetch(model.glb) L->>L: HTTP GET → ArrayBuffer L->>P: parse(arrayBuffer) P->>P: Header 12B 확인<br/>(magic=glTF, version=2) P->>P: JSON 청크 분리<br/>(0x4E4F534A) P->>P: BIN 청크 분리<br/>(0x004E4942) P->>P: JSON.parse() → glTFRoot P->>P: buffers 해석<br/>(BIN 청크 or fetch URI) P->>P: bufferView 슬라이스 생성 P->>P: accessor → TypedArray 뷰 loop primitive마다 P->>G: createBuffer(VBO/IBO) P->>G: bufferData(attribute) end loop image마다 P->>P: decodeImage()<br/>(PNG/JPEG/KTX2) P->>G: createTexture()<br/>+ sampler 적용 end P->>P: scene 그래프 빌드<br/>(node transform 캐시) P-->>L: GLTFScene 객체 반환 L-->>U: scene 핸들 전달 U->>G: render(scene, camera) G->>G: 노드 순회 → drawCall
Step 1 — GLB 바이트 분해
function parseGlb(arrayBuffer) {
const view = new DataView(arrayBuffer);
if (view.getUint32(0, true) !== 0x46546C67) throw new Error('Not a GLB');
const version = view.getUint32(4, true); // 2
const length = view.getUint32(8, true);
let offset = 12;
let json, bin;
while (offset < length) {
const chunkLen = view.getUint32(offset, true);
const chunkType = view.getUint32(offset + 4, true);
const dataStart = offset + 8;
if (chunkType === 0x4E4F534A) { // JSON
json = new TextDecoder('utf-8').decode(
new Uint8Array(arrayBuffer, dataStart, chunkLen)
);
} else if (chunkType === 0x004E4942) { // BIN
bin = arrayBuffer.slice(dataStart, dataStart + chunkLen);
}
offset = dataStart + chunkLen;
}
return { json, bin };
}Step 4 — bufferView → accessor 해석 (핵심)
bufferView: { buffer: 0, byteOffset: 0, byteLength: 288, target: 34962 }
accessor: { bufferView: 0, byteOffset: 0, componentType: 5126, type: "VEC3", count: 24 }
buffer[0]: ArrayBuffer(360)
→ positions = new Float32Array(buffer, 0 + 0, 24 * 3) = [...72 floats]
Step 5 — 시맨틱 → 셰이더 attribute (Three.js GLTFLoader 예시)
| glTF 시맨틱 | Three.js attribute 이름 |
|---|---|
POSITION | position |
NORMAL | normal |
TANGENT | tangent |
TEXCOORD_0 | uv |
TEXCOORD_1 | uv1 |
COLOR_0 | color |
JOINTS_0 | skinIndex |
WEIGHTS_0 | skinWeight |
Step 7 — 드로우 콜 의사 코드
function renderNode(node, parentWorld) {
const local = node.matrix
? mat4.clone(node.matrix)
: mat4.fromRotationTranslationScale(out, node.rotation, node.translation, node.scale);
const world = mat4.multiply(out2, parentWorld, local);
if (node.mesh !== undefined) {
for (const primitive of meshes[node.mesh].primitives) {
bindMaterial(primitive.material);
bindAttributes(primitive.attributes);
bindIndices(primitive.indices);
setUniform('u_ModelMatrix', world);
gl.drawElements(primitive.mode, indexCount, indexType, 0);
}
}
for (const childIdx of node.children || []) {
renderNode(nodes[childIdx], world);
}
}디버깅 시 자주 보게 되는 함정
| 증상 | 원인 | 해결 |
|---|---|---|
| 모델이 회전 누워서 보임 | Y-up ↔ Z-up 변환 누락 또는 중복 | 3D Tiles 클라이언트가 자동 변환 — 추가 회전 행렬 넣지 말 것 |
| 모델이 화면 어딘가 사라짐 | accessor.min/max 누락 → 컬링 박스 잘못 계산 | POSITION accessor에 정확한 min/max 채울 것 |
| 모델이 떨림 (jitter) | ECEF 좌표를 float32에 그대로 저장 → 정밀도 손실 | tile.transform으로 중심 빼고 vertex는 상대 좌표로 (상세) |
| 텍스처가 자글자글/aliasing | sampler.minFilter 미설정 → Mipmap 무시 | minFilter: 9987 (LINEAR_MIPMAP_LINEAR) 명시 (보정 코드) |
| 색이 너무 어둡거나 밝음 | sRGB ↔ linear 색공간 혼동 | baseColor/emissive는 sRGB, 나머지는 linear |
| roughness/metallic 채널 뒤바뀜 | R/G/B 채널 매핑 실수 | G=roughness, B=metallic 고정 |
| Three.js에서 일부 모델 로드 실패 | extensionsRequired에 미지원 확장 포함 | KHR_draco는 DRACOLoader, KHR_texture_basisu는 KTX2Loader 등록 필요 |
| Cesium에서 모델만 뜨고 클릭 안 됨 | EXT_mesh_features 없이 b3dm batchId만 있음 | 3d-tiles-tools로 1.1 업그레이드 (상세) |
검증 체크리스트
gltf-validator통과 — 오류·경고 0건accessor.min/max가 모든 POSITION에 있음extensionsRequired에 명시된 확장이 타깃 클라이언트에서 지원됨- sampler가 모든 텍스처에 명시적 설정됨 (minFilter 포함)
- material의 alphaMode가 의도와 일치 (특히 OPAQUE인데 BLEND 쓰면 성능 저하)
- bufferView·accessor의 정렬 규칙 준수
JSON 최상위 객체 한눈에 보기
{
"asset": { "version": "2.0", "generator": "Blender 3.6" },
"scene": 0,
"scenes": [{ "nodes": [0] }],
"nodes": [{ "mesh": 0, "translation": [0, 0, 0] }],
"meshes": [{ "primitives": [{ "attributes": { "POSITION": 0 }, "indices": 1, "material": 0 }] }],
"accessors": [
{ "bufferView": 0, "componentType": 5126, "count": 24, "type": "VEC3", "min": [-1,-1,-1], "max": [1,1,1] },
{ "bufferView": 1, "componentType": 5123, "count": 36, "type": "SCALAR" }
],
"bufferViews":[
{ "buffer": 0, "byteOffset": 0, "byteLength": 288, "target": 34962 },
{ "buffer": 0, "byteOffset": 288, "byteLength": 72, "target": 34963 }
],
"buffers": [{ "byteLength": 360 }],
"materials": [{ "pbrMetallicRoughness": { "baseColorFactor": [0.8, 0.2, 0.2, 1.0] } }],
"extensionsUsed": ["KHR_materials_unlit"],
"extensionsRequired": []
}이 작은 예제 하나로 모든 핵심 요소(scene/node/mesh/primitive/accessor/bufferView/buffer/material/extensions)가 등장한다. 실전 GLB는 이 구조의 반복일 뿐이다.