이번에는 표준 포맷인 glTF 2.0 모델을 불러와서, 지오메트리와 PBR 머티리얼, 텍스처를 엔진 구조로 옮깁니다. 테스트 모델은 Khronose 공식 샘플인 DamagedHelemt입니다.
- 왜 glTF인가
- PBR metallic-rouoghness 머티리얼이 표준 사양에 들어 있습니다.
- .gltf(JSON과 외부 파일)와 .glb(바이너리 하나로 묶음) 두 형태가 있습니다.
- 파서로는 단일 헤더 라이브러리인 cgltf를 썼습니다.
전체흐름
cgltf_parse_file + cgltf_load_buffers
├─ 1) 텍스처: 실제로 쓰이는 이미지만, (이미지, sRGB 여부) 단위로 캐시
├─ 2) 머티리얼: glTF material → Material (Scene 소유)
├─ 3) 메시: 프리미티브마다 Mesh 생성 + 머티리얼과 짝지음
└─ 4) 노드: 메시가 달린 노드마다 Entity 생성 (월드 행렬 → Transform)
Model 클래스는 메시와 텍스처를 소유하고, 씬에는 머티리얼과 엔티티를 만들어 넣었습니다. 씬이 모델의 메시를 참조하므로 씬을 쓰는 동안 Model이 살아 있어야 합니다.
1.에셋 경로 정리
경로가 하드코딩 되어 있어서 소스 포러 기준으로 절대 경로를 쓰도록 바꿨습니다.
inline std::string AssetPath(const std::string& relative)
{
return std::string(BAEK_ROOT_DIR "Assets/") + relative;
}
2. 지오메트리
glTF의 구조
- mesh 하나는 primitive 여러 개로 이루어집니다. 프리미티브는 머티리얼 하나를 쓰는 조각입니다.
- node는 변환과 계층을 가지고 메시를 참조합니다. 같은 메시를 여러 노드가 공유할 수 있습니다.
- 정점 데이터는 accessor를 통해 읽습니다. cgltf_accessor_read_float를 쓰면 저장 타입(float, 정규화된 정수 등)과 무관하게 float로 받을 수 있습니다.
32비트 인덱스
16비트 인덱스는 정점 65,536개까지만 가리킬 수 있습니다 실제 모델에는 부족해서 Mesh의 인덱스를 전부 32비트(DXGI_FORMAT_R32_UINT)로 통일했습니다.
오른손->왼손 좌표계 변환
glTF는 오른손 좌표계, 왼손은 왼손 좌표계입니다. 그대로 쓰면 모델이 거울상으로 나옵니다.
| 대상 | 변환 | 이유 |
| 위치, 노멀, 탄젠트 | Z 성분 부호 반전 | 좌표계 뒤집기 |
| 탄젠트 w | 부호 반전 | 거울 반전하면 외적 방향이 뒤집힘 |
| 인덱스 | 삼각형마다 두개 교환 | Z를 뒤집으면 도는 방향이 반대가 됨 |
| 노드 행렬 | S x N x S (S=Z 반전) | 정점과 같은 기준으로 맞춤 |
노드 행렬 변환은 이렇게 유도됩니다. 원래 w = v·M 이고 변환 후 정점은 v' = v·S입니다.
변환된 월드 좌표 w' = w·S = v'·(S·M·S)가 되므로, 새 행렬은 S·M·S입니다.
행렬 -> Transform 분해
glTF노드는 변환을 행렬로 줍니다. 엔티티는 위치/회전/스케일을 들고 있으므로 분해가 필요합니다.
- 위치: 마지막 행
- 스케일: 앞 세 행 각각의 길이
- 회전: 스케일을 나눈 행렬에서 오일러 각을 추출합니다. 이때 Transform::Matrix()와 같은 회전 순서 (X->Y->Z)로 분해해야 합니다.
3. 머티리얼과 텍스처
내장 이미지
.glb는 이미지가 파일 안에 PNG/JPG 바이트 그대로 들어 있습니다.
stbi_load_from_memory로 디코딩하는 경로를 추가했습니다.
.gltf는 옆에 있는 이미지 파일을 URI로 참조하므로, %20같은 인코딩을 풀고 모델 폴더 기준으로 경로를 만듭니다.
색 공간
| 데이터 | glTF에서의 색 공간 | 처리 |
| baseColor, emissive 텍스처 | sRGB | _SRGB 포맷으로 업로드 |
| normal, metallic-roughness 텍스처 | 선형 | 일반 포맷 |
| baseColorFactor | 선형 | 엔진 Material은 sRGB 기준이라 로딩 시 변환 |
| emissiveFactor | 선형 | 그대로 사용 |
같은 이미지가 용도에 따라 다른 포맷이 될 수 있어서, 텍스처 캐시 키를 (이미지, sRGB 여부) 쌍으로 잡았습니다.
탄젠트 생성
DamagedHelment은 파일에 탄젠트가 없습니다. 노멀 맵을 쓰려면 직접 계산해야 합니다.
- 삼각형마다 두 변과 UV변화량으로 U방향(T)과 V방향(B)을 구해 세 정점에 누적합니다.
- 정점마다 T를 노멀에 수직이 되게 다듬고(그람-슈미트), 정규화합니다.
- cross(N, T)와 누적된 B를 비교해 비탄젠트 부호(w)를 정합니다.
주의할 점: glTF 노멀 맵은 초록 채널이 이미지 위쪽입니다. 즉, V가 줄어는 방향입니다. 엔진의 절차적 테스트 텍스처는 반대 규약(V가 늘어나는 방향)을 썼습니다. 두 규약이 공존할 수 있는 건 셰이더가 B = cross(N, T) x w로 계산하고, w로 방향을 정하기 때문입니다.
Emissive
자체 발광 맵(t3)을 추가했습니다.
emissiveFactor x emissiveTexture를 조명 결과에 더합니다. 기본 factor가 0이라서, 맵이 없을 때 흰색 텍스처를 바인딩해도 발광하지 않습니다.
'DirectX12 > 엔진 만들어보기' 카테고리의 다른 글
| cgltf API 정리 (사용한것 기준) (0) | 2026.10.09 |
|---|---|
| 라이팅에서 PBR 머티리얼까지 (0) | 2026.10.06 |
| 그리드, Scene/Entity, Hierarchy·Inspector, 기즈모 (0) | 2026.10.05 |
| 서브모듈 정리, 카메라 (0) | 2026.10.03 |
| Viewport 만들기 (0) | 2026.10.02 |



