액세서
고친 사람 github-actions[bot]
액세서는 바이트 덩이 속 숫자를 무슨 값으로 몇 개 읽을지 알려 줍니다. 3차원 모델 파일 형식인 glTF 에서 좌표를 꺼낼 때 늘 이것을 거칩니다. 객체 지향 프로그래밍에서는 필드를 읽고 쓰는 메서드를 같은 이름으로 부릅니다. 이 항목은 glTF 의 뜻을 다룹니다.
쉽고 빠른 이해
액세서는 3차원 모델 파일 형식인 glTF 에서 바이트 덩이에 붙이는 「읽는 법」 쪽지입니다. 「4바이트 실수를 셋씩 묶어 한 점으로 보고, 그런 점을 100개 읽어라」처럼 적습니다.
이게 없으면 바이트는 숫자 더미일 뿐입니다. 같은 4바이트도 실수로 읽느냐 정수로 읽느냐에 따라 전혀 다른 값이 됩니다. 읽는 법을 따로 적어 두면 큰 바이트 덩이는 손대지 않고 그래픽 카드로 올릴 수 있습니다.
어떻게 도나:
- 모델의 좌표와 순서 번호를 바이트 덩이 하나에 이어 붙입니다. 순서 번호는 어느 꼭짓점 셋을 이어 삼각형을 만들지 적은 번호입니다
- 액세서가 「어느 토막에서, 무슨 값으로, 몇 개」를 적습니다. 토막은 바이트 덩이의 한 구간입니다
- 모델을 그리는 쪽은 좌표가 필요하면 좌표 액세서의 번호만 따라갑니다
glTF 에서 좌표·순서 번호·애니메이션 값처럼 숫자로 쪼개 읽는 이진 데이터는 전부 액세서를 거칩니다. 통째로 풀어야 하는 이미지 파일은 거치지 않습니다.
대가는 거쳐야 할 단계가 는다는 점입니다. 값 하나를 찾으려면 액세서와 토막과 덩이를 차례로 지나야 합니다. 그중 숫자 하나만 틀려도 엉뚱한 바이트를 읽습니다.
상세
이 절은 액세서가 무엇을 적는지부터 봅니다. 예는 삼각형 하나를 담은 작은 파일입니다. 원소 하나의 크기를 셈하는 법, 섞어 담은 토막에서 값을 골라 읽는 법, 정수를 실수로 펴 읽는 법을 차례로 봅니다.
그다음 액세서 번호를 적어 데이터를 꺼내는 객체들과, 읽는 프로그램이 액세서를 다루는 모습을 봅니다. 끝으로 같은 이름을 쓰는 객체 지향 프로그래밍의 뜻을 가릅니다.
숫자 더미와 읽는 법
숫자만 빼곡히 적힌 종이 한 장을 받았다고 해 봅시다. 옆에 붙은 쪽지에 「세 개씩 끊어 읽으세요. 한 묶음이 한 사람의 키, 몸무게, 나이입니다. 모두 쉰 명입니다」라고 적혀 있습니다. 쪽지가 없으면 그 종이는 읽을 수 없는 숫자 더미입니다.
glTF(GL Transmission Format)는 3차원 장면을 프로그램 사이에 실어 나르는 파일 형식입니다. 장면의 구조는 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) 텍스트로 적습니다. 좌표 같은 숫자 뭉치는 이진 덩이에 따로 담습니다. 이 이진 덩이를 버퍼라고 부릅니다.
삼각형 모델이라면 버퍼에는 꼭짓점 좌표와 순서 번호가 들어갑니다. 순서 번호는 어느 꼭짓점 셋을 이어 삼각형 하나를 만들지 적은 번호입니다. 번호로 이으면 이웃한 삼각형이 같은 꼭짓점을 나눠 쓸 수 있습니다.
버퍼는 바이트가 한 줄로 이어진 것이라 스스로 뜻을 말하지 않습니다. 같은 4바이트도 실수 하나로 읽으면 1.0 이 되고, 부호 없는 정수 하나로 읽으면 1065353216 이 됩니다.
액세서는 이 바이트를 어떤 값으로 몇 개 읽을지 적은 기록입니다. 액세서는 JSON 쪽의
accessors 배열에 들어 있습니다. 다른 객체는 이 배열의 번호로 액세서를 부릅니다.
읽는 법을 바이트와 떼어 둔 덕에 바이트는 손대지 않아도 됩니다. 파일을 여는 프로그램은 바이트를 담긴 모양 그대로 GPU(Graphics Processing Unit, 그래픽 처리 장치)에 올립니다. 액세서에 적힌 읽는 법은 함께 넘깁니다.
액세서가 답하는 세 물음
액세서 하나는 세 물음에 답합니다. 어디서 읽나, 한 번에 무엇을 읽나, 몇 번 읽나입니다.
첫째는 어디서입니다. 버퍼 하나에는 여러 데이터가 섞여 들어 있어서 먼저 토막을 가려야 합니다.
시작 바이트와 길이로 버퍼 안의 토막 하나를 가리키는 기록이 버퍼 뷰입니다. 액세서는
bufferView 에 버퍼 뷰 번호를 적습니다. byteOffset 에는 그 토막 안에서 몇 바이트 뒤부터
읽을지를 적습니다.
둘째는 한 번에 무엇을 읽나입니다. 한 번에 읽는 묶음을 원소라고 부릅니다. 점 하나의 좌표라면 x·y·z 세 수가 한 원소입니다. 원소 안의 수 하나하나는 성분이라고 부릅니다.
componentType 은 성분 하나가 어떤 자료형인지 적습니다. type 은 성분 몇 개가 한 원소인지
적습니다. 둘을 합치면 원소 하나의 모양이 정해집니다.
셋째는 몇 번입니다. count 는 원소가 몇 개인지 적습니다. 바이트 수도 성분 수도 아닙니다.
점 100개의 좌표라면 성분은 300개입니다. count 는 100 입니다.
삼각형 꼭짓점 셋의 좌표를 읽는 액세서를 적으면 아래와 같습니다.
"accessors": [
{ "bufferView": 0, // 0번 토막에서
"byteOffset": 0, // 토막 맨 앞부터
"componentType": 5126, // 4바이트 실수
"type": "VEC3", // 셋이 한 원소
"count": 3 } // 원소 3개
]
이 액세서는 0번 버퍼 뷰의 맨 앞부터 4바이트 실수를 셋씩 묶어 세 번 읽습니다. 액세서가 적을 수 있는 칸은 이 다섯 말고도 몇 개 더 있습니다. 아래 소절들에서 하나씩 봅니다.
| 칸 | 무엇을 적나 | 안 적으면 |
|---|---|---|
bufferView |
몇 번 버퍼 뷰를 읽나 | 모든 값을 0 으로 채운다(희소 액세서에서 쓴다 · 아래) |
byteOffset |
버퍼 뷰 안에서 몇 바이트 뒤부터 읽나 | 0 |
componentType |
성분 하나의 자료형 | 꼭 적는다 |
type |
성분 몇 개가 한 원소인가 | 꼭 적는다 |
count |
원소가 몇 개인가 | 꼭 적는다 |
normalized |
정수 성분을 실수 범위로 펴 읽나 | 펴지 않는다 |
min · max |
성분마다 가장 작은 값과 가장 큰 값 | 대개 안 적어도 된다 |
sparse |
바뀐 원소만 따로 적은 목록 | 없는 것으로 본다 |
원소 하나의 크기
읽는 프로그램은 원소 하나가 몇 바이트인지 알아야 다음 원소로 넘어갈 수 있습니다. 원소 크기는 성분 하나의 크기에 성분 수를 곱한 값입니다.
성분의 자료형은 여섯 가지입니다. componentType 에는 이름 대신 번호를 적습니다.
| 번호 | 자료형 | 바이트 |
|---|---|---|
| 5120 | 부호 있는 8비트 정수 | 1 |
| 5121 | 부호 없는 8비트 정수 | 1 |
| 5122 | 부호 있는 16비트 정수 | 2 |
| 5123 | 부호 없는 16비트 정수 | 2 |
| 5125 | 부호 없는 32비트 정수 | 4 |
| 5126 | 32비트 실수 | 4 |
이 번호는 배열 첨자가 아닙니다. 그래픽 프로그래밍 도구인 OpenGL(Open Graphics Library) 이 자료형에 매겨 쓰던 번호를 빌려 온 값입니다. 부호 있는 32비트 정수는 목록에 없습니다.
type 은 성분 수를 이름으로 적습니다. VEC 은 벡터(vector), MAT 은 행렬(matrix)의
줄임입니다. SCALAR 는 수 하나를 뜻합니다.
type |
성분 수 | 담는 값의 예 |
|---|---|---|
SCALAR |
1 | 순서 번호 |
VEC2 |
2 | 평면 위 좌표(x·y) |
VEC3 |
3 | 좌표(x·y·z) |
VEC4 |
4 | 색(빨강·초록·파랑·투명도) |
MAT2 |
4 | — |
MAT3 |
9 | — |
MAT4 |
16 | 점을 옮기고 돌리는 4×4 행렬 |
앞의 삼각형 예로 셈해 봅니다.
const comp = 4; // 5126 · 실수
const elem = comp * 3; // 12 · VEC3
const total = elem * 3; // 36 · count 3
원소 하나가 12바이트라 셋이면 36바이트입니다. 이 범위가 버퍼 뷰 밖으로 넘치면 그 파일은 잘못된 파일입니다.
섞어 담은 토막에서 골라 읽기
한 꼭짓점의 좌표와 법선을 번갈아 이어 담는 파일이 많습니다. 법선은 그 점에서 면이 어느 쪽을 향하는지를 나타내는 방향 값입니다. 이렇게 섞어 담으면 한 꼭짓점의 값들이 메모리에 붙어 있게 됩니다.
좌표 12바이트와 법선 12바이트가 꼭짓점마다 번갈아 놓인 토막은 이렇게 생겼습니다. 한 줄이 꼭짓점 하나 몫인 24바이트입니다.
block-beta columns 2 p0["좌표 0 · 0~11"] n0["법선 0 · 12~23"] p1["좌표 1 · 24~35"] n1["법선 1 · 36~47"] p2["좌표 2 · 48~59"] n2["법선 2 · 60~71"]
이 토막 하나를 액세서 둘이 나눠 읽습니다. 좌표 액세서는 토막 맨 앞부터, 법선 액세서는 12바이트 뒤부터 읽기 시작합니다.
| 좌표 액세서 | 법선 액세서 | |
|---|---|---|
bufferView |
0 | 0 |
byteOffset |
0 | 12 |
| 다음 원소까지 | 24바이트 | 24바이트 |
둘 다 원소 하나를 읽고 나면 24바이트를 건너뛰어 다음 꼭짓점으로 갑니다. 이 건너뛰는 거리를
스트라이드라고 부릅니다. 스트라이드는 액세서가 아니라 버퍼 뷰의 byteStride 에 적습니다.
그 토막을 읽는 액세서 모두에 같은 값이 걸립니다.
꼭짓점 값이 아닌 데이터는 섞어 담지 않습니다. 순서 번호가 그렇습니다. 이런 데이터는 원소가 빈틈없이 붙어 있다고 보고 읽습니다.
정수를 실수로 펴 읽기
색이나 텍스처 좌표는 0 과 1 사이 실수면 충분합니다. 텍스처 좌표는 표면의 한 점이 그림의 어디에 해당하는지 적은 값입니다. 이런 값을 4바이트 실수 대신 1바이트 정수로 담으면 크기가 4분의 1 로 줄어듭니다.
normalized 가 참이면 읽는 쪽이 정수 성분을 실수 범위로 펴서 읽습니다. 부호 없는 정수는
0~1 로, 부호 있는 정수는 -1~1 로 폅니다. 부호 없는 8비트 정수라면 255 로 나눈 값입니다.
255 / 255; // 1
128 / 255; // 0.50196…
0 / 255; // 0
실수 성분과 부호 없는 32비트 정수 성분에는 이 칸을 참으로 둘 수 없습니다.
최솟값과 최댓값
min 과 max 는 성분마다 가장 작은 값과 가장 큰 값을 적습니다. VEC3 좌표라면 x·y·z 각각의
값이라 두 배열의 길이가 셋입니다. 적은 값은 버퍼에 든 실제 값과 맞아야 합니다.
좌표의 min 과 max 를 두 꼭짓점으로 삼으면 모델을 감싸는 상자가 나옵니다. 이런 상자를
바운딩 박스라고 합니다. 파일을 여는 쪽은 바이트를 한 번도 읽지 않고 모델이 차지하는 공간을
압니다.
꼭짓점 좌표를 담는 액세서는 이 두 칸을 꼭 적습니다. 다른 액세서는 대개 적지 않아도 됩니다.
바뀐 원소만 적는 희소 액세서
얼굴 모델에서 입꼬리만 올리는 표정처럼, 모델의 일부만 움직이는 모양이 있습니다. 기본 모양에서 몇 꼭짓점만 옮겨 만든 모양을 모프 타깃이라고 합니다. 꼭짓점 1만 개 가운데 열 개만 옮겼는데 1만 개를 다 적으면 거의 전부가 기본 모양과 같은 값입니다.
sparse 는 바뀐 원소만 따로 적습니다. 이런 액세서를 희소 액세서라고 부릅니다. sparse 에는
바뀐 원소 수(count), 바뀐 원소의 번호(indices), 바뀐 값(values) 셋이 들어갑니다. 번호는
작은 것부터 커지는 순서로 적습니다.
읽는 쪽은 먼저 기본 배열을 만듭니다. bufferView 가 있으면 거기서 읽습니다. 없으면 전부 0 으로
채웁니다. 그 위에 번호가 가리키는 원소만 바뀐 값으로 덮어씁니다.
const base = [0, 0, 0, 0, 0]; // count 5
const idx = [1, 3]; // 바뀐 번호
const val = [7, 9]; // 바뀐 값
idx.forEach((i, k) => { base[i] = val[k]; });
base; // [0, 7, 0, 9, 0]
액세서 번호를 적는 객체들
glTF 에서 성분으로 쪼개 읽는 이진 데이터는 전부 액세서를 거칩니다. 메시, 애니메이션, 스킨이 그런 데이터를 씁니다. 셋 다 바이트 위치를 직접 적지 않고 액세서 번호만 적습니다.
메시는 모델의 겉모양을 이루는 삼각형 묶음입니다. 사람 모델이라면 몸의 겉면이 메시로 적힙니다.
메시는 한 번에 그리는 단위로 쪼개져 있습니다. 그 조각 하나를 프리미티브라고 부릅니다. 재질이 다른 곳, 이를테면 옷과 피부는 서로 다른 프리미티브가 됩니다.
정점 속성은 꼭짓점 하나가 들고 있는 값의 종류입니다. 정점은 그래픽스에서 꼭짓점을 부르는 말입니다. 좌표와 법선이 정점 속성입니다.
프리미티브는 정점 속성 이름마다 액세서 번호를 하나씩 답니다. POSITION 은 좌표를, NORMAL 은
법선을 담은 액세서를 가리킵니다.
앞에서 순서 번호라 부른 것, 곧 삼각형을 어느 꼭짓점 순서로 잇는지 적은 번호 목록을 그래픽스에서는
인덱스라고 합니다. 이 항목도 여기서부터 인덱스라고 부릅니다. 인덱스도 indices 칸에 액세서
번호 하나로 답니다.
"primitives": [
{ "attributes": {
"POSITION": 1, // 1번 액세서
"NORMAL": 2 }, // 2번 액세서
"indices": 0 } // 0번 액세서
]
이 프리미티브는 1번 액세서에서 좌표를, 2번에서 법선을, 0번에서 인덱스를 읽습니다. 인덱스
액세서는 SCALAR 이고 부호 없는 정수만 씁니다. 부호 없는 32비트 정수는 인덱스에만 쓸 수
있습니다.
인덱스에는 그 자료형이 담을 수 있는 가장 큰 값을 넣지 않습니다. 8비트면 255, 16비트면 65535 입니다. 까닭은 이어 그리기라는 그리는 방식에 있습니다.
이어 그리기는 앞 삼각형의 꼭짓점 둘을 이어 받고 새 꼭짓점 하나만 더해 다음 삼각형을 그리는 방식입니다. 꼭짓점을 덜 적어도 되지만, 떨어진 두 조각을 한 목록으로 그리려면 「여기서 끊고 새로 시작하라」는 신호가 따로 있어야 합니다.
일부 그래픽 API(Application Programming Interface, 응용 프로그래밍 인터페이스)는 인덱스의 가장 큰 값을 이 신호로 읽습니다. 이 동작을 프리미티브 재시작이라고 부릅니다. 그래서 glTF 파일은 이 값을 꼭짓점 번호로 쓰지 않습니다.
애니메이션은 시각마다 값이 어떻게 바뀌는지 적습니다. 시각 목록과 값 목록을 액세서 둘로
따로 읽습니다. 시각 쪽 액세서는 SCALAR 실수입니다. 이 액세서에도 min 과 max 를 꼭 적습니다.
스킨은 뼈대를 움직이면 거기 붙은 표면이 따라 휘게 하는 설정입니다. 스킨은 뼈대마다 점을
옮기고 돌리는 4×4 행렬 하나를 액세서로 읽습니다. MAT4 원소 하나가 뼈대 하나 몫입니다.
세 객체가 액세서를 거쳐 바이트에 닿는 길을 모으면 이렇습니다.
flowchart TD
subgraph 부르는객체["액세서 번호를 적는 객체"]
P["프리미티브 · 정점 속성과 인덱스"]
A["애니메이션 · 시각과 값"]
S["스킨 · 뼈대마다 4×4 행렬"]
end
P --> X["액세서 · 읽는 법"]
A --> X
S --> X
X --> V["버퍼 뷰 · 토막"]
V --> B["버퍼 · 이진 덩이"]
쓰임새가 셋이어도 바이트를 읽는 길은 하나입니다. 파일을 여는 프로그램은 액세서 읽는 코드를 한 번만 짜면 됩니다.
텍스처 이미지는 이 길을 타지 않습니다. 텍스처는 모델 표면에 입히는 그림입니다. 이 그림은 PNG(Portable Network Graphics)나 JPEG(Joint Photographic Experts Group) 파일로 담깁니다.
이런 이미지 파일은 성분으로 쪼개 읽는 데이터가 아니라 통째로 풀어야 하는 파일입니다. 그래서 이미지는 액세서 없이 버퍼 뷰를 바로 가리킵니다.
타입 배열로 읽기
자바스크립트로 glTF 를 읽는 프로그램은 액세서 하나를 타입 배열 하나로 바꿉니다. 타입 배열은
바이트 덩이 위에 자료형을 얹어 배열처럼 읽게 해 주는 객체입니다. 5126 이면 Float32Array,
5123 이면 Uint16Array 가 짝입니다.
아래는 버퍼 뷰가 버퍼의 72바이트 뒤에서 시작하고, 그 안에 좌표가 빈틈없이 붙어 있는 경우입니다.
bin 은 버퍼 전체를 담은 바이트 덩이입니다. 타입 배열은 길이를 원소 수가 아니라 성분 수로
받습니다. count 3 에 VEC3 이면 3 × 3 = 9 를 넘깁니다.
const start = 72 + 0; // 뷰 + 액세서
const pos = new Float32Array(bin, start, 9);
pos.length; // 9
pos[3]; // 둘째 점의 x
Float32Array 는 시작 바이트가 4의 배수일 때만 만들어집니다. glTF 가 액세서 시작 위치를 성분
크기의 배수로 맞추게 하는 까닭이 여기 있습니다. 맞춰 두면 복사 없이 버퍼 위에 바로 얹을 수
있습니다. 꼭짓점 값은 더 엄격해서 시작 위치와 스트라이드가 모두 4의 배수여야 합니다.
섞어 담은 토막이면 타입 배열을 한 번 얹는 것으로 끝나지 않습니다. 원소마다 스트라이드만큼 건너뛰며 읽어야 합니다.
버퍼의 수는 전부 가장 낮은 바이트를 앞에 두는 순서로 적습니다. 바이트를 놓는 순서를 엔디언이라고 합니다. 가장 낮은 바이트를 앞에 두는 순서는 리틀 엔디언입니다.
타입 배열은 그 컴퓨터 자신의 바이트 순서로 값을 읽습니다. 흔히 쓰는 컴퓨터는 대부분 리틀 엔디언이라서, 타입 배열을 버퍼 위에 얹기만 하면 순서를 뒤집지 않고도 바른 값이 나옵니다.
단계가 늘어나는 대가
좌표 하나를 찾으려면 프리미티브에서 액세서로, 액세서에서 버퍼 뷰로, 버퍼 뷰에서 버퍼로 세 번 건너가야 합니다. 번호 하나나 오프셋 하나만 틀려도 파일은 열립니다. 열린 채로 엉뚱한 바이트를 읽습니다. 그러면 모델이 찌그러지거나 점이 흩어져 그려집니다.
count 에 바이트 수를 적는 실수도 이렇게 드러납니다. 원소 수보다 큰 값이 들어가면 읽는 범위가
버퍼 뷰 밖으로 넘칩니다. 그런 파일은 잘못된 파일입니다.
같은 이름의 다른 뜻
객체 지향 프로그래밍에서 액세서(accessor)는 객체의 필드를 읽거나 바꾸는 메서드입니다. 읽는 쪽을 게터, 바꾸는 쪽을 세터라고 부릅니다. 한국어 문서에서는 흔히 접근자라고 옮깁니다.
두 뜻 모두 날것의 저장소 앞에 읽는 길을 하나 세운다는 점은 닮았습니다. 객체 지향의 뜻은 접근자 항목이 다룹니다.
관련 항목
액세서가 읽어 들이는 원본 데이터
버퍼 · 버퍼 뷰 · 바이트 · GLB · 이진 데이터
액세서가 읽는 법을 적는 칸
컴포넌트 타입 · 스트라이드 · 오프셋 · 메모리 정렬 · 정규화된 정수 · 바운딩 박스 · 희소 액세서 · 엔디언
액세서 번호를 적어 데이터를 꺼내는 glTF 객체
프리미티브 · 메시 · 정점 속성 · 인덱스 · 모프 타깃 · 애니메이션 · 키프레임 · 스킨 · 스키닝
액세서로 읽는 값
정점 · 법선 · 텍스처 좌표 · 정점 색 · 벡터 · 행렬 · 스칼라
액세서가 속하는 파일 포맷
액세서가 데이터를 넘겨 주는 그래픽 API
GPU · OpenGL · WebGL · Vulkan · 정점 버퍼 · 인덱스 버퍼 · 프리미티브 재시작 · 셰이더
액세서를 바이트 위에 얹어 읽는 언어 도구
타입 배열 · ArrayBuffer · DataView · Float32Array
이름이 같은 객체 지향 프로그래밍 용어
접근자 · 게터 · 세터 · 프로퍼티 · 캡슐화 · 필드 · 메서드
액세서가 속하는 상위 분류
다른 이름: accessor · glTF accessor