프리미티브 토폴로지
고친 사람 github-actions[bot]
프리미티브 토폴로지는 그래픽 칩에 넘긴 꼭짓점들을 어떤 도형으로 이을지 정해 줍니다. 같은 꼭짓점 여섯 개도 이 규칙에 따라 삼각형 두 개가 됩니다. 규칙을 바꾸면 꺾인 선 하나가 됩니다. 그리기를 시킬 때 꼭짓점들과 함께 넘기는 값입니다.
쉽고 빠른 이해
프리미티브 토폴로지는 「이 꼭짓점들을 셋씩 끊어 삼각형으로 봐라」 같은 묶는 규칙입니다. 꼭짓점 0부터 5까지를 넘기면서 이 규칙을 고르면 삼각형 (0,1,2) 와 (3,4,5) 가 나옵니다.
그래픽 칩은 꼭짓점들을 한 줄로 늘어선 배열로 받습니다. 배열에는 어디서 한 도형이 끝나는지가 적혀 있지 않습니다. 이 규칙이 없으면 칩은 꼭짓점을 어떻게 이어야 할지 모릅니다.
어떻게 도나:
- 프로그램이 꼭짓점 배열과 묶는 규칙을 함께 넘깁니다
- 칩이 규칙대로 꼭짓점을 끊거나 이어 점·선·삼각형을 만듭니다
- 만들어진 도형마다 화면에서 덮는 픽셀을 칠합니다
대가가 있습니다. 이웃 도형과 꼭짓점을 나눠 쓰는 규칙을 고르면 넘길 꼭짓점이 줄어듭니다. 대신 한 줄로 이어진 모양만 그릴 수 있습니다. 떨어진 조각을 그리려면 그리기를 여러 번 시키거나 끊는 표시를 넣어야 합니다.
상세
구슬 여섯 개가 한 줄로 놓여 있습니다. 「셋씩 떼어 고리를 만드세요」라는 쪽지가 붙으면 고리 두 개가 나옵니다. 「처음부터 끝까지 한 줄로 꿰세요」라는 쪽지가 붙으면 목걸이 하나가 나옵니다. 구슬은 같고 쪽지만 바뀌었습니다.
이 절은 프리미티브 토폴로지가 무엇을 정하고 왜 따로 알려 줘야 하는지 봅니다. 번호 0부터 5까지의 꼭짓점 여섯 개를 여러 규칙으로 묶어 보는 예를 줄곧 씁니다. 뒤쪽 소절에서는 규칙마다 치르는 값과 코드에서 규칙을 고르는 곳을 봅니다.
경계가 적혀 있지 않은 정점 배열
화면에 그리는 일은 그래픽 칩인 GPU(Graphics Processing Unit, 그래픽 처리 장치)가 맡습니다. GPU 는 꼭짓점을 이어 도형을 만듭니다. 그리고 그 도형이 덮는 픽셀을 칠합니다.
프리미티브는 GPU 가 한 번에 칠하는 가장 작은 도형입니다. 점·선·삼각형이 프리미티브입니다. 복잡한 모양도 결국 이 셋을 여러 개 모아 그립니다.
앞에서 꼭짓점이라 부른 것, 곧 프리미티브의 꼭짓점 하나가 정점입니다. 정점은 위치 좌표를 기본으로 담은 값 묶음입니다. 색이나 표면의 방향 같은 값을 함께 담기도 합니다.
프로그램은 정점을 배열 하나에 줄지어 담아 GPU 에 넘깁니다. 이 배열을 담는 메모리가 정점 버퍼입니다. 배열에는 정점만 이어져 있습니다. 어디까지가 한 도형인지는 적혀 있지 않습니다. 어느 정점끼리 이어지는지도 적혀 있지 않습니다.
그래서 프로그램이 「이 배열을 이런 규칙으로 묶어라」를 따로 알려 줍니다. 이 묶는 규칙이 프리미티브 토폴로지입니다. 같은 배열이라도 규칙을 바꾸면 전혀 다른 그림이 나옵니다.
토폴로지는 원래 수학의 한 분야인 위상수학을 가리키는 말입니다. 그래픽스에서는 점들이 서로 어떻게 이어졌는지, 곧 연결 관계를 이 말로 부릅니다. 정점이 어디 놓였는지가 아니라 어느 정점이 어느 정점과 이어지는지를 정한다는 뜻입니다.
목록과 스트립과 팬
규칙의 이름은 도형 종류와 묶는 방식을 짝지어 붙입니다. 「삼각형」과 「스트립」을 짝지으면 삼각형 스트립입니다. 묶는 방식은 크게 셋입니다.
목록(list)은 정점을 정해진 개수씩 끊어 도형 하나를 만듭니다. 도형끼리 정점을 나눠 쓰지 않습니다. 삼각형 목록이면 정점 셋이 삼각형 하나입니다.
스트립(strip)은 띠라는 뜻입니다. 새 정점 하나가 바로 앞 정점들과 이어져 도형 하나를 만듭니다. 삼각형 스트립이면 새 정점이 앞의 두 정점과 삼각형을 이룹니다. 이웃한 삼각형이 변 하나를 함께 쓰는 띠 모양이 나옵니다.
팬(fan)은 부채라는 뜻입니다. 삼각형 팬은 첫 정점 하나를 모든 삼각형이 함께 씁니다. 첫 정점을 가운데 두고 삼각형이 부채처럼 둘러 펼쳐집니다.
아래 표는 흔히 쓰는 규칙 여섯을 견줍니다. 정점 n 개를 넣었을 때 도형이 몇 개 나오는지와, 이 절의 예인 정점 여섯 개를 넣었을 때의 결과를 나란히 적었습니다.
| 규칙 | 묶는 법 | 정점 n 개로 | 정점 6개로 |
|---|---|---|---|
| 점 목록 | 정점 하나가 점 하나 | 점 n개 | 점 6개 |
| 선 목록 | 둘씩 끊어 선 하나 | 선 n/2개 | 선 3개 |
| 선 스트립 | 앞 정점과 이어 선 하나 | 선 n−1개 | 선 5개 |
| 삼각형 목록 | 셋씩 끊어 삼각형 하나 | 삼각형 n/3개 | 삼각형 2개 |
| 삼각형 스트립 | 새 정점이 앞의 둘과 삼각형 하나 | 삼각형 n−2개 | 삼각형 4개 |
| 삼각형 팬 | 첫 정점·앞 정점·새 정점으로 삼각형 하나 | 삼각형 n−2개 | 삼각형 4개 |
규칙을 고르면 나오는 프리미티브의 종류도 함께 정해집니다. 삼각형 스트립을 고르면 삼각형만 나옵니다. 선 목록을 고르면 선만 나옵니다. 그래서 그래픽스 코드에서 「프리미티브 종류」를 고르라는 인자와 「토폴로지」를 고르라는 인자는 같은 것을 묻습니다.
같은 정점 여섯 개를 삼각형 목록·삼각형 스트립·삼각형 팬으로 묶으면 삼각형이 이렇게 나옵니다. 그림은 어느 정점이 한 삼각형에 드는지만 보입니다. 정점을 읽는 차례는 뒤의 「스트립과 감김 순서」 소절에서 봅니다.
flowchart TD
V["정점 배열 · 0 1 2 3 4 5"]
subgraph L["삼각형 목록 · 셋씩 끊기"]
L1["0,1,2"]
L2["3,4,5"]
end
subgraph S["삼각형 스트립 · 앞의 둘을 물려받기"]
S1["0,1,2"] --> S2["1,2,3"] --> S3["2,3,4"] --> S4["3,4,5"]
end
subgraph F["삼각형 팬 · 첫 정점을 함께 쓰기"]
F1["0,1,2"] --> F2["0,2,3"] --> F3["0,3,4"] --> F4["0,4,5"]
end
V --> L
V --> S
V --> F
목록은 정점 셋마다 끊으니 삼각형이 둘입니다. 스트립은 정점 2 부터 새 정점이 들어올 때마다 삼각형이 하나씩 생기니 넷입니다. 팬도 넷입니다. 네 삼각형 모두 정점 0 을 함께 씁니다.
규칙이 쓰이는 단계
GPU 가 정점을 받아 픽셀을 칠하기까지 거치는 단계 전체가 그래픽스 파이프라인입니다. 토폴로지 규칙은 이 흐름의 맨 앞에서 넘겨받습니다. 그런데 쓰이는 것은 몇 단계 뒤입니다.
맨 앞의 입력 어셈블러는 버퍼에서 정점을 하나씩 꺼내는 단계입니다. 정점을 꺼낼 때 묶는 규칙도 함께 받아 둡니다.
셰이더는 GPU 에서 실행되는 작은 프로그램입니다. 파이프라인의 몇몇 단계는 이 프로그램이 맡습니다.
입력 어셈블러 다음 단계가 정점 셰이더입니다. 정점 셰이더는 정점마다 한 번씩 실행되어 그 정점이 화면 어디에 놓일지 계산합니다.
정점 셰이더는 정점을 하나씩 따로 다룹니다. 그래서 이 단계는 아직 어느 정점이 어느 도형에 속하는지 모릅니다.
그다음 프리미티브 조립 단계가 토폴로지 규칙대로 정점을 묶습니다. 여기서 비로소 도형이 생깁니다.
도형이 생긴 뒤에야 그 도형이 덮는 픽셀을 찾을 수 있습니다. 픽셀을 찾는 이 단계가 래스터화입니다.
아래 그림은 규칙이 들어오는 단계와 쓰이는 단계가 떨어져 있음을 보입니다. 점선이 규칙이 건너가는 길입니다.
flowchart TD
IA["입력 어셈블러 · 정점을 꺼내고 규칙을 받는다"]
VS["정점 셰이더 · 정점을 하나씩 옮긴다"]
PA["프리미티브 조립 · 규칙대로 묶는다"]
R["래스터화 · 도형이 덮는 픽셀을 찾는다"]
IA --> VS --> PA --> R
IA -. 토폴로지 규칙 .-> PA
스트립이 아끼는 정점
삼각형 여러 개가 변을 맞대고 한 줄로 이어진 면을 그린다고 해 봅니다. 길이나 강처럼 긴 띠 모양의 바닥이 그렇습니다. 삼각형 k 개를 목록으로 그리면 정점 3k 개를 넘깁니다. 스트립으로 그리면 k+2 개면 됩니다.
삼각형 100개라면 목록은 정점 300개, 스트립은 102개입니다. 차이는 맞닿은 변에서 납니다. 두 삼각형이 함께 쓰는 변의 정점 둘을 목록은 삼각형마다 다시 넘깁니다. 스트립은 한 번만 넘깁니다.
정점 하나에는 위치와 색 같은 값이 여럿 붙습니다. 그래서 정점 수가 줄면 넘기는 데이터가 그만큼 줄어듭니다.
대가는 모양입니다. 스트립 하나는 한 줄로 이어진 띠만 만듭니다. 떨어진 두 조각이나 중간에 가지가 갈라지는 모양은 스트립 하나로 그릴 수 없습니다.
인덱스로 정점을 다시 쓰기
정점을 아끼는 길이 하나 더 있습니다. 정점마다 번호를 붙여 두는 방식입니다. 도형은 그 번호를 늘어놓은 목록으로 적습니다. 이 번호가 인덱스입니다.
번호 목록을 담는 메모리는 인덱스 버퍼입니다. 정점을 담는 정점 버퍼와 따로 둡니다.
인덱스를 쓰면 토폴로지 규칙은 정점 배열이 아니라 번호 목록에 걸립니다. 번호 0,1,2,2,1,3 을 삼각형 목록으로 묶으면 삼각형 (0,1,2) 와 (2,1,3) 이 나옵니다. 번호 1 과 2 가 두 번씩 나와도 정점 데이터는 한 벌만 있으면 됩니다.
아래 그림은 이 예에서 번호가 정점을 가리키는 모양을 보입니다. 삼각형 목록 규칙은 위쪽 번호 목록에 걸려 번호를 셋씩 묶습니다. 정점 1 과 2 에는 화살표가 두 개씩 모입니다.
flowchart TD
subgraph IB["인덱스 버퍼 · 번호 목록"]
subgraph T1["삼각형 (0,1,2)"]
I0["0"]
I1["1"]
I2["2"]
end
subgraph T2["삼각형 (2,1,3)"]
I3["2"]
I4["1"]
I5["3"]
end
end
subgraph VB["정점 버퍼"]
V0["정점 0"]
V1["정점 1"]
V2["정점 2"]
V3["정점 3"]
end
I0 --> V0
I1 --> V1
I2 --> V2
I3 --> V2
I4 --> V1
I5 --> V3
그래서 인덱스를 쓰면 목록 규칙으로도 정점 데이터를 다시 쓸 수 있습니다. 이때 스트립이 더 줄여 주는 것은 번호의 개수입니다. 번호 하나는 정수 하나라서 정점 하나보다 훨씬 작습니다.
이런 까닭에 모양이 제각각인 물체는 흔히 인덱스와 삼각형 목록을 함께 써서 그립니다. 목록은 모양에 제약이 없기 때문입니다. 스트립은 지형이나 길처럼 원래 띠로 이어진 모양에 잘 맞습니다.
스트립과 감김 순서
삼각형의 세 정점을 읽는 차례가 화면에서 시계 방향으로 도는지 반시계 방향으로 도는지를 감김 순서라고 부릅니다. GPU 는 이 방향을 보고 삼각형의 앞면과 뒷면을 가립니다.
카메라를 등진 뒷면은 어차피 화면에 보이지 않습니다. 그래서 GPU 는 뒷면 삼각형을 칠하기 전에 버려 일을 줄입니다. 이 일을 뒷면 컬링이라고 부릅니다.
띠 모양을 만들려면 스트립의 정점을 띠의 위 줄과 아래 줄에 번갈아 놓습니다. 정점 0·2·4 는 위 줄에, 정점 1·3·5 는 아래 줄에 섭니다. 새 정점이 반대쪽 줄에 놓일 때마다 띠가 삼각형 하나만큼 자랍니다.
아래 그림은 그 배치입니다. 화살표는 번호 순서대로 정점을 따라가는 길입니다.
block-beta columns 6 p0["0"] space p2["2"] space p4["4"] space space p1["1"] space p3["3"] space p5["5"] p0 --> p1 p1 --> p2 p2 --> p3 p3 --> p4 p4 --> p5
첫 삼각형 0,1,2 를 따라가 봅니다. 위 줄에서 아래 줄로 내려갔다가 다시 위 줄로 올라갑니다. 이 삼각형은 반시계 방향으로 돕니다.
둘째 삼각형 1,2,3 은 거꾸로 아래 줄에서 시작합니다. 위 줄로 올라갔다가 다시 아래 줄로 내려옵니다. 이 삼각형은 시계 방향으로 돕니다.
스트립을 번호 순서대로만 읽으면 이렇게 방향이 삼각형마다 뒤집힙니다. 그러면 띠의 삼각형이 하나 걸러 뒷면이 되어 버려집니다.
그래서 스트립 규칙은 차례를 고쳐 읽습니다. 둘째·넷째처럼 하나 걸러 오는 삼각형은 앞의 두 정점 차례를 맞바꿉니다. 아래 표는 정점 여섯 개로 만든 삼각형 넷을 두 가지 차례로 나란히 적었습니다.
| 삼각형 | 번호 순서대로 | 고쳐 읽은 차례 |
|---|---|---|
| 첫째 | 0,1,2 | 0,1,2 |
| 둘째 | 1,2,3 | 2,1,3 |
| 셋째 | 2,3,4 | 2,3,4 |
| 넷째 | 3,4,5 | 4,3,5 |
둘째 삼각형을 2,1,3 으로 읽으면 위 줄의 2 에서 아래 줄의 1 로 내려갑니다. 그다음 아래 줄을 따라 3 으로 갑니다. 첫째와 같은 반시계 방향입니다. 고쳐 읽은 차례로는 네 삼각형이 모두 같은 방향으로 돕니다.
아래 함수는 번호 배열을 삼각형 스트립으로 묶는 과정을 흉내 냅니다. 반복 변수 i 는 삼각형의 마지막 정점이 배열의 몇 번째인지를 가리킵니다.
function strip(idx) {
const tris = [];
for (let i = 2; i < idx.length; i++) {
const [a, b, c] = [idx[i-2], idx[i-1], idx[i]];
tris.push(i % 2 === 0 ? [a, b, c] : [b, a, c]);
}
return tris;
}
strip([0, 1, 2, 3]); // [[0,1,2],[2,1,3]]
i 가 2 이면 첫째 삼각형, 3 이면 둘째 삼각형입니다. 그래서 i 가 홀수일 때가 둘째·넷째 삼각형입니다. 그때만 a 와 b 를 맞바꿉니다. 결과의 둘째 삼각형 [2,1,3] 은 앞 소절에서 삼각형 목록으로 적은 둘째 삼각형과 같습니다. 두 규칙이 같은 방향으로 도는 삼각형을 내는 것입니다.
띠를 끊는 특수 번호
스트립 하나는 이어진 띠 하나만 그립니다. 떨어진 띠 여러 개를 그리려면 띠마다 그리기 명령을 따로 불러야 합니다. 이 명령 한 번을 드로 콜이라고 부릅니다. 명령이 늘면 명령을 준비하고 넘기는 일도 늘어납니다.
인덱스를 쓰면 번호 목록 중간에 특수한 번호를 넣어 띠를 끊을 수 있습니다. GPU 는 이 번호를 만나면 지금 띠를 끝냅니다. 다음 번호부터는 새 띠를 시작합니다. 이 기능을 프리미티브 재시작이라고 부릅니다.
예를 들어 번호 목록 0,1,2,3 뒤에 특수 번호를 넣고 4,5,6,7 을 이어 삼각형 스트립으로 묶어 봅니다. 띠가 둘로 갈립니다. 아래 그림은 어느 정점이 한 삼각형에 드는지만 보입니다.
flowchart TD
N["번호 목록 · 0 1 2 3 · 특수 번호 · 4 5 6 7"]
subgraph A["띠 1"]
A1["0,1,2"] --> A2["1,2,3"]
end
X["특수 번호 · 지금 띠를 끝낸다"]
subgraph B["띠 2"]
B1["4,5,6"] --> B2["5,6,7"]
end
N --> A
A --> X --> B
정점 3 과 4 는 서로 다른 띠에 있어서 둘을 잇는 삼각형이 생기지 않습니다. 드로 콜 한 번으로 띠 두 개를 그린 것입니다.
특수 번호로는 번호의 자료형이 담을 수 있는 가장 큰 값을 흔히 씁니다. 번호를 16비트 부호 없는 정수로 적으면 이 값은 65535 입니다. 32비트면 4294967295 입니다. 재시작을 켜 두면 이 값은 정점 번호로 쓰지 못합니다.
특수한 용도의 토폴로지
앞의 여섯 규칙 말고도, 특정 단계를 쓸 때만 고르는 규칙이 둘 있습니다. 둘 다 정점 셰이더 뒤에 붙일 수 있는 단계를 위한 것입니다.
지오메트리 셰이더는 도형 하나를 받아 새 도형을 만들어 내는 단계입니다. 물체의 윤곽선을 찾을 때처럼 이웃 도형의 정점까지 봐야 하는 경우가 있습니다.
그럴 때 고르는 규칙이 인접 정보 프리미티브입니다. 도형마다 자기 정점에 더해 이웃 도형의 정점을 함께 딸려 보냅니다.
테셀레이션은 도형을 잘게 쪼개 면을 더 매끄럽게 만드는 단계입니다. 가까이 보이는 면만 촘촘하게 쪼개는 식으로 씁니다.
테셀레이션을 쓸 때는 정점 여러 개를 한 덩이의 패치로 묶는 규칙을 고릅니다. 한 덩이에 정점을 몇 개 넣을지는 프로그램이 정합니다.
코드에서 규칙을 고르는 곳
코드에서는 토폴로지 규칙을 상수나 문자열 하나로 고릅니다. 그래픽스 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)에 따라 그리기 명령을 부를 때마다 넘기기도 합니다. 그리기 설정 묶음을 만들 때 미리 적어 두기도 합니다.
브라우저에서 GPU 를 쓰는 API 가 둘 있습니다. WebGL 은 그리기 명령의 인자로 규칙을 받습니다. WebGPU 는 렌더 파이프라인을 만들 때 규칙을 받습니다.
렌더 파이프라인은 앞에서 본 그래픽스 파이프라인의 각 단계를 어떻게 돌릴지 적어 둔 설정 묶음입니다. 아래 표는 두 API 가 같은 규칙을 어떤 이름으로 부르는지 보입니다.
| 규칙 | WebGL | WebGPU |
|---|---|---|
| 점 목록 | gl.POINTS |
"point-list" |
| 선 목록 | gl.LINES |
"line-list" |
| 선 스트립 | gl.LINE_STRIP |
"line-strip" |
| 삼각형 목록 | gl.TRIANGLES |
"triangle-list" |
| 삼각형 스트립 | gl.TRIANGLE_STRIP |
"triangle-strip" |
| 삼각형 팬 | gl.TRIANGLE_FAN |
없음 |
WebGPU 처럼 삼각형 팬이 빠진 API 도 있습니다. 팬으로 그리던 모양은 삼각형 목록으로 풀어 적으면 됩니다. 팬의 삼각형 (0,1,2) 와 (0,2,3) 은 번호 목록 0,1,2,0,2,3 을 삼각형 목록으로 묶어 그립니다.
관련 항목
프리미티브 토폴로지의 하위 종류
점 목록 · 선 목록 · 선 스트립 · 삼각형 목록 · 삼각형 스트립 · 삼각형 팬 · 인접 정보 프리미티브 · 패치
프리미티브 토폴로지가 묶는 데이터
정점 · 정점 버퍼 · 정점 속성 · 인덱스 · 인덱스 버퍼 · 프리미티브
프리미티브 토폴로지를 받아 쓰는 파이프라인 단계
그래픽스 파이프라인 · 입력 어셈블러 · 정점 셰이더 · 프리미티브 조립 · 지오메트리 셰이더 · 테셀레이션 · 래스터화 · 컬링 · 셰이더
스트립 규칙에 딸린 기법
감김 순서 · 프리미티브 재시작 · 퇴화 삼각형 · 정점 캐시 · 드로 콜
프리미티브 토폴로지를 고르는 그래픽스 API
WebGL · WebGPU · OpenGL · Vulkan · Direct3D · Metal · 렌더 파이프라인
프리미티브 토폴로지가 속하는 상위 분류
토폴로지라는 이름을 함께 쓰는 개념
위상수학 · 네트워크 토폴로지 · 메시 토폴로지
다른 이름: primitive topology · Primitive Topology