테셀레이션 제어 셰이더
고친 사람 github-actions[bot]
테셀레이션 제어 셰이더는 도형 하나를 몇 토막으로 쪼갤지 정하는 프로그램입니다. 그래픽 카드는 그 수만큼 도형을 작은 삼각형 여럿으로 나눕니다. 가까이 보이는 땅은 촘촘하게, 멀리 보이는 땅은 성기게 나누라고 도형마다 따로 정할 수 있습니다. 덕분에 성긴 모형 하나만 넘겨도 가까운 곳은 매끄럽게 그려집니다.
쉽고 빠른 이해
테셀레이션 제어 셰이더는 도형 한 묶음을 받아 「몇 토막으로 쪼개라」는 수를 적어 내는 짧은 프로그램입니다. 카메라 바로 앞의 바위는 변마다 열여섯 토막, 저 멀리 있는 바위는 두 토막으로 적는 식입니다.
이게 없으면 가까이 볼 때를 대비해 처음부터 촘촘한 모형을 넘겨야 합니다. 멀리 있어 점처럼 보이는 물체도 똑같이 촘촘한 모형을 씁니다. 쪼갤 수를 그릴 때마다 정하면 성긴 모형 하나로 가까운 곳만 촘촘하게 그릴 수 있습니다.
어떻게 도나:
- 앱이 성긴 모형을 몇 점씩 묶어 넘깁니다
- 이 프로그램이 묶음마다 변을 몇 토막 낼지 적습니다
- 그래픽 카드가 그 수대로 잘게 쪼갭니다. 다음 단계가 새로 생긴 점의 위치를 정합니다
대가는 쪼갤수록 삼각형이 늘어 뒤 단계의 일이 늘어난다는 것입니다. 이웃한 묶음끼리 맞닿은 변을 서로 다르게 쪼개면 그 사이가 벌어져 틈이 보입니다. 모형이 이미 촘촘하거나 늘 멀리서만 보이면 쓸 일이 없습니다.
상세
케이크를 몇 조각으로 자를지 정하는 사람과 칼을 드는 사람이 따로 있다고 해 봅시다. 앞사람은 판마다 손님 수를 보고 「이 판은 여덟 조각, 저 판은 네 조각」이라고 적어 넘깁니다. 칼을 든 사람은 적힌 수대로 자르기만 합니다.
테셀레이션 제어 셰이더는 앞사람입니다. 도형마다 몇 토막으로 쪼갤지 수를 적어 넘깁니다. 쪼개는 일은 그래픽 카드에 박힌 고정 장치가 합니다. 이 절은 그 수를 무엇을 보고 정하는지, 누구에게 넘기는지를 그림 몇 장과 코드 한 벌로 봅니다.
테셀레이션이라는 말
테셀레이션은 원래 평면을 같은 모양의 조각으로 빈틈없이 덮는 일을 가리킵니다. 그래픽스에서는 큰 도형 하나를 작은 삼각형 여럿으로 나누는 일을 가리킵니다.
화면의 3D 모형은 삼각형을 이어 붙여 만듭니다. 삼각형의 꼭짓점을 그래픽스에서는 정점이라고 부릅니다.
삼각형이 많을수록 둥근 면이 매끄럽게 보입니다. 대신 꼭짓점이 늘어난 만큼 메모리를 더 씁니다. 그림 계산은 그래픽 카드(GPU, Graphics Processing Unit, 그래픽 처리 장치)라는 별도 장치가 맡습니다. 모형은 그리기 전에 그래픽 카드로 옮겨야 하므로 옮길 양도 늘어납니다.
테셀레이션은 이 문제를 뒤집습니다. 성긴 모형만 옮기고 그래픽 카드 안에서 잘게 쪼갭니다. 옮기는 양은 적은 채로 둡니다. 화면에 보이는 삼각형만 늘어납니다.
쪼개는 단위인 패치
쪼개는 단위는 삼각형 하나가 아니라 패치입니다. 패치는 꼭짓점 몇 개를 한 묶음으로 묶은 것입니다. 몇 개씩 묶을지는 앱이 정해서 넘깁니다.
테셀레이션에서는 패치에 든 꼭짓점을 제어점이라고 부릅니다. 새로 생길 점들의 위치를 계산할 때 기준으로 쓰는 점이라서 붙은 이름입니다.
패치는 쪼개는 모양에 따라 삼각형 패치와 사각형 패치로 나뉩니다. 삼각형 패치는 삼각형 하나를, 사각형 패치는 사각형 하나를 잘게 쪼갭니다. 어느 모양으로 쪼갤지는 셰이더 코드에 적어 둡니다.
제어점 열여섯 개를 가로 넷, 세로 넷으로 늘어놓은 사각형 패치도 씁니다. 이 열여섯 점이 휘어진 곡면 한 장의 모양을 정합니다. 이런 곡면이 베지어 곡면입니다.
그래픽스 파이프라인 속의 위치
그림을 그리는 흐름은 정해진 순서의 단계로 이어집니다. 이 흐름 전체가 그래픽스 파이프라인입니다. 몇몇 단계는 비어 있어서 만드는 쪽이 짧은 프로그램을 적어 꽂습니다. 그렇게 꽂는 프로그램을 통틀어 셰이더라고 합니다.
테셀레이션은 한 단계가 아니라 세 단계가 한 조로 돕니다. 가운데 단계만 그래픽 카드에 박힌 고정 장치입니다. 앞뒤 두 단계는 셰이더입니다.
flowchart TD
A["정점 셰이더 · 제어점 하나마다"] --> T
subgraph T["테셀레이션 세 단계"]
B["테셀레이션 제어 셰이더 · 몇 토막인지 정한다"]
B --> C["테셀레이터 · 정한 수대로 쪼갠다"]
C --> D["테셀레이션 평가 셰이더 · 새 점의 위치를 정한다"]
end
T --> E["래스터화 · 덮인 픽셀을 고른다"]
E --> F["프래그먼트 셰이더 · 픽셀마다 색을 정한다"]
정점 셰이더는 꼭짓점 하나마다 한 번씩 도는 셰이더입니다. 테셀레이션을 쓸 때는 제어점 하나마다 돌며 위치를 옮깁니다.
테셀레이션 제어 셰이더가 그 뒤에 옵니다. 패치 하나를 한 덩이로 받아 「몇 토막」이라는 수를 적어 넘깁니다.
테셀레이터는 그 수를 받아 패치를 작은 삼각형들로 쪼개는 고정 장치입니다. 여기에는 프로그램을 꽂을 수 없습니다.
테셀레이터는 새 점의 실제 위치를 모릅니다. 패치 안에서 어느 비율쯤에 놓이는지만 셉니다. 한 변을 네 토막 내면 새 점은 그 변의 4분의 1, 2분의 1, 4분의 3 지점에 놓입니다.
테셀레이션 평가 셰이더가 그 비율과 제어점을 받아 새 점마다 실제 위치를 계산합니다. 제어점이 휘어진 곡면을 정하면 새 점을 그 곡면 위로 끌어올립니다. 그래서 곧던 변이 둥글게 휩니다.
평가 셰이더는 땅의 높낮이를 적은 그림을 읽어 점을 들어 올리기도 합니다. 이 기법이 변위 매핑입니다. 쪼갠 점마다 높이를 달리 주므로 성긴 모형에서 울퉁불퉁한 땅이 나옵니다.
그 뒤로는 테셀레이션이 없을 때와 같습니다. 래스터화가 삼각형이 덮은 픽셀을 고릅니다. 프래그먼트 셰이더가 그 픽셀마다 색을 정합니다.
그래픽 카드에 그리기를 시키는 프로그래밍 규격을 그래픽 API(Application Programming Interface, 애플리케이션 프로그래밍 인터페이스)라고 합니다. OpenGL·Vulkan·Direct3D가 그런 규격입니다.
이 글이 쓰는 단계 이름은 OpenGL과 Vulkan의 것입니다. Direct3D는 제어 셰이더를 헐 셰이더, 평가 셰이더를 도메인 셰이더라고 부릅니다.
내놓는 값 둘
이 셰이더가 내놓는 것은 둘입니다. 다음 단계로 넘길 제어점과 쪼갤 수입니다.
제어점은 받은 것을 손대지 않고 넘겨도 됩니다. 옮기거나 개수를 바꿔 넘겨도 됩니다. 삼각형 하나의 제어점 셋을 받아 휘어진 곡면용 제어점 여럿으로 바꿔 내보내는 쓰임이 그 예입니다.
쪼갤 수를 테셀레이션 레벨이라고 합니다. 바깥 레벨과 안쪽 레벨 두 종류가 있습니다.
바깥 레벨은 패치의 변 하나를 몇 토막 낼지 정합니다. 변마다 하나씩 적으므로 삼각형 패치는 셋, 사각형 패치는 넷을 적습니다.
안쪽 레벨은 패치 안쪽을 몇 칸으로 나눌지 정합니다. 사각형 패치는 가로와 세로를 따로 나누므로 둘을 적습니다. 삼각형 패치는 하나를 적습니다.
아래는 사각형 패치의 안쪽 레벨을 가로 3, 세로 2로 적은 예입니다. 위아래 변의 바깥 레벨도 3, 좌우 변도 2로 맞추면 패치는 가로 세 칸, 세로 두 칸으로 나뉩니다. 칸마다 삼각형 둘이 들어갑니다.
block-beta columns 3 c1["칸"] c2["칸"] c3["칸"] c4["칸"] c5["칸"] c6["칸"]
패치 모양마다 적는 수를 모으면 이렇습니다.
| 패치 모양 | 바깥 레벨 | 안쪽 레벨 |
|---|---|---|
| 삼각형 | 셋 · 변마다 하나 | 하나 |
| 사각형 | 넷 · 변마다 하나 | 둘 · 가로와 세로 |
변마다 수를 따로 두는 까닭은 아래 「이웃 패치와 맞닿은 변」에서 봅니다.
바깥 레벨 가운데 하나라도 0 이하로 적으면 그 패치는 버려집니다. 화면 밖에 있거나 카메라를 등진 패치를 이 단계에서 걸러 내면 뒤 단계의 일이 줄어듭니다.
한 패치를 여럿이 나눠 도는 방식
이 셰이더는 패치 하나에 한 번만 도는 게 아닙니다. 내보낼 제어점 하나마다 한 번씩 돕니다. 제어점 셋을 내보내면 같은 패치를 두고 세 번 돕니다. 한 번 도는 것을 호출이라고 부릅니다.
flowchart TD
P["패치 하나 · 입력 제어점 셋"] --> I0["호출 0"]
P --> I1["호출 1"]
P --> I2["호출 2"]
I0 --> O0["출력 제어점 0"]
I1 --> O1["출력 제어점 1"]
I2 --> O2["출력 제어점 2"]
I0 --> L["쪼갤 수 · 패치에 한 벌"]
호출마다 자기 몫의 출력 제어점 하나를 씁니다. 읽기는 다릅니다. 호출 셋이 모두 패치의 입력 제어점 전부를 읽을 수 있습니다. 꼭짓점 하나만 보는 정점 셰이더와 다른 점입니다.
쪼갤 수는 패치 하나에 한 벌입니다. 그래서 호출 가운데 하나만 적으면 됩니다. 흔히 번호가 0인 호출이 맡습니다.
다른 호출이 쓴 출력을 읽어야 할 때도 있습니다. 그때는 모든 호출이 쓰기를 마칠 때까지 기다리는 명령을 먼저 부릅니다. 이렇게 서로 기다려 발을 맞추는 지점이 배리어입니다. 기다리지 않고 읽으면 아직 안 쓴 값을 읽을 수 있습니다.
코드 한 벌
셰이더를 적는 언어는 그래픽 API마다 다릅니다. 아래는 GLSL(OpenGL Shading Language, OpenGL 셰이딩 언어)로 적은 예입니다. 삼각형 패치를 받아 제어점은 손대지 않고 넘깁니다. 세 변과 안쪽을 모두 네 토막으로 쪼개라고 적습니다.
layout(vertices = 3) out;
uniform float n; // 앱이 4.0 을 넣음
void main() {
int i = gl_InvocationID; // 0, 1, 2
vec4 p = gl_in[i].gl_Position; // 받은 점
gl_out[i].gl_Position = p; // 내보냄
if (i == 0) {
gl_TessLevelOuter[0] = n; // 변0 4토막
gl_TessLevelOuter[1] = n; // 변1 4토막
gl_TessLevelOuter[2] = n; // 변2 4토막
gl_TessLevelInner[0] = n; // 안쪽 4
}
}
첫 줄은 내보낼 제어점이 셋이라는 약속입니다. 그래서 이 프로그램은 패치 하나마다 세 번 돕니다.
n 은 앱이 그리기 전에 넣어 주는 값입니다. 모든 호출이 같은 값을 읽습니다. 이런 값을 유니폼 변수라고 부릅니다.
gl_InvocationID 는 지금 몇 번째 호출인지를 알려 줍니다. 세 호출이 각자 0, 1, 2 를 받습니다.
gl_in 은 들어온 제어점 배열입니다. gl_out 은 내보낼 제어점 배열입니다. 각 호출은 자기 번호의 입력을 읽어 같은 번호의 출력에 적습니다.
gl_Position 은 점의 위치입니다. vec4 는 값 네 개짜리 묶음입니다. 그래픽스는 위치를 x·y·z 에 값 하나를 더한 넷으로 적습니다.
아래 네 줄이 쪼갤 수입니다. gl_TessLevelOuter 셋이 세 변의 바깥 레벨입니다. gl_TessLevelInner 하나가 안쪽 레벨입니다. 삼각형 패치라서 앞의 표대로 셋과 하나를 적습니다.
이 네 줄은 0번 호출만 적습니다. 패치 하나에 한 벌이면 되기 때문입니다.
쪼갤 수를 정하는 기준
위 코드는 모든 패치를 같은 수로 쪼갭니다. 보통은 이 수를 패치마다 그릴 때 계산합니다. 이 소절은 무엇을 보고 그 수를 고르는지를 봅니다.
가장 흔한 기준은 카메라와의 거리입니다. 카메라 바로 앞의 바위는 변마다 열여섯 토막, 저 멀리 있는 바위는 두 토막으로 적는 식입니다. 멀리 있는 물체는 화면에 작게 찍힙니다. 잘게 쪼개도 눈에 띄는 차이가 없습니다.
거리 대신 변이 화면에서 몇 픽셀 길이로 보이는지를 재기도 합니다. 화면에서 긴 변일수록 더 잘게 쪼갭니다. 쪼갠 삼각형이 화면에서 비슷한 크기가 되도록 맞추는 방법입니다.
보이는 크기에 맞춰 세밀함을 바꾸는 기법을 통틀어 레벨 오브 디테일이라고 합니다. 모형을 여러 벌 미리 만들어 두고 거리마다 바꿔 끼우는 방법도 이 이름으로 묶입니다. 테셀레이션은 모형 한 벌로 세밀함을 조금씩 바꿉니다.
이웃 패치와 맞닿은 변
모형은 패치를 이어 붙여 만듭니다. 그래서 패치 둘이 변 하나를 나눠 가지는 일이 흔합니다.
그 변을 패치 A 는 네 토막, 패치 B 는 두 토막으로 쪼갰다고 해 봅시다. 네 토막 쪽에는 두 토막 쪽에 없는 점이 생깁니다.
flowchart TD
subgraph A["패치 A 쪽 변 · 네 토막"]
A0["끝점 P"] --- A1["두 토막 쪽에 없는 점"]
A1 --- A2["가운데 점"]
A2 --- A3["두 토막 쪽에 없는 점"]
A3 --- A4["끝점 Q"]
end
subgraph B["패치 B 쪽 변 · 두 토막"]
B0["끝점 P"] --- B1["가운데 점"]
B1 --- B2["끝점 Q"]
end
A ~~~ B
두 토막 쪽 변에는 4분의 1 지점과 4분의 3 지점에 점이 없습니다. 그래서 그 변은 곧게 남습니다.
평가 셰이더가 네 토막 쪽의 점만 곡면을 따라 끌어올리면 두 패치 사이가 벌어집니다. 화면에는 가느다란 틈으로 보입니다. 이 틈이 크랙입니다.
막는 방법은 맞닿은 변의 바깥 레벨을 양쪽이 똑같이 적는 것입니다. 그래서 그 변의 바깥 레벨은 두 끝점만 보고 계산합니다. 두 끝점은 양쪽 패치가 똑같이 가진 값입니다. 같은 값으로 같은 계산을 하면 같은 수가 나옵니다.
패치 가운데까지의 거리로 계산하면 이 약속이 깨집니다. 두 패치의 가운데가 서로 달라서 같은 변에 다른 수가 나옵니다. 바깥 레벨을 변마다 따로 두는 까닭이 이것입니다. 안쪽 레벨은 이웃과 맞닿지 않으므로 패치 혼자 정해도 됩니다.
쪼갤수록 늘어나는 비용
쪼갤 수를 올리면 삼각형이 빠르게 늘어납니다. 변을 두 배로 잘게 쪼개면 삼각형 수는 대략 네 배가 됩니다. 가로와 세로 두 방향으로 함께 늘기 때문입니다.
늘어난 삼각형은 뒤 단계 모두의 일이 됩니다. 평가 셰이더는 새 점마다 한 번씩 돕니다. 래스터화와 프래그먼트 셰이더도 삼각형이 늘어난 만큼 일이 붙습니다.
삼각형이 픽셀 몇 개 크기보다 작아지면 손해가 커집니다. 그래픽 카드는 픽셀을 몇 개씩 묶어 한꺼번에 칠합니다. 삼각형이 묶음 속 픽셀 하나만 덮어도 묶음 전체를 계산합니다.
그래서 아주 작은 삼각형은 덮은 픽셀보다 많은 일을 씁니다. 쪼갤 수에 위쪽 한도를 두는 까닭이 이것입니다.
테셀레이션을 안 쓰는 편이 나은 경우도 있습니다. 모형이 이미 촘촘하거나 늘 멀리서만 보이면 쪼갤 일이 없습니다. 그때 세 단계를 켜 두면 패치를 거쳐 가는 일만 늘어납니다.
관련 항목
테셀레이션 제어 셰이더 앞뒤에 놓이는 처리 단계
그래픽스 파이프라인 · 입력 어셈블러 · 정점 셰이더 · 테셀레이터 · 테셀레이션 평가 셰이더 · 지오메트리 셰이더 · 프리미티브 조립 · 클리핑 · 래스터화 · 프래그먼트 셰이더 · 출력 병합기
테셀레이션 제어 셰이더가 받고 내놓는 값
패치 · 제어점 · 테셀레이션 레벨 · 정점 · 프리미티브 · 메시
같은 파이프라인의 다른 단계에 꽂히는 셰이더
셰이더 · 도메인 셰이더 · 픽셀 셰이더 · 컴퓨트 셰이더 · 메시 셰이더 · 태스크 셰이더
쪼갤 수를 정하는 기준과 기법
테셀레이션 · 레벨 오브 디테일 · 적응형 테셀레이션 · 화면 공간 오차 · 프러스텀 컬링 · 후면 컬링
테셀레이션으로 만드는 표면 표현
변위 매핑 · 높이 맵 · 지형 렌더링 · 베지어 곡면 · 세분 곡면 · PN 삼각형 · 법선 매핑
이웃 패치 사이에서 생기는 결함
크랙 · T-정션 · 워터타이트 메시 · 부동소수점 오차
테셀레이션 제어 셰이더 비용을 키우는 요인
프리미티브 수 · 작은 삼각형 문제 · 쿼드 오버셰이딩 · 오버드로 · 병목
호출끼리 발을 맞추는 동기화 수단
배리어 · 동기화 · 공유 메모리 · 병렬 처리
테셀레이션 제어 셰이더를 적는 언어와 그 처리
GLSL · HLSL · SPIR-V · 셰이더 컴파일 · 유니폼 변수
테셀레이션 제어 셰이더를 규정하는 그래픽 API
OpenGL · OpenGL ES · Direct3D · Vulkan · API
이 단계를 돌리는 그래픽 카드와 그 성질
GPU · 셰이더 코어 · 고정 기능 단계 · SIMD · 픽셀
테셀레이션 제어 셰이더가 속하는 상위 분류
다른 이름: tessellation control shader · TCS · 헐 셰이더 · hull shader