샘플러
샘플러는 그림에서 색을 어떻게 읽어 올지 미리 정해 둡니다. 좌표 하나를 건네받으면 그 규칙대로 그림의 칸 몇 개를 읽어 색 하나로 만들어 돌려줍니다. 규칙은 그릴 때마다 고르지 않고 한 벌로 묶어 만들어 둡니다.
쉽고 빠른 이해
샘플러는 그림에서 색을 꺼내는 방식을 적어 둔 설정 묶음입니다. 벽돌 그림을 넓은 벽에 되풀이해 붙입니다. 가까이 다가가면 칸 경계가 부드럽게 번지도록 맞춰 둡니다.
그림은 네모 칸이 모인 격자입니다. 그런데 그릴 때 물어보는 지점은 칸과 칸 사이에 떨어집니다. 무엇을 돌려줄지 미리 정해 두지 않으면 답이 없습니다.
어떻게 도나:
- 물어본 지점이 그림 밖이면 정해 둔 방식으로 안쪽 지점으로 되돌립니다
- 화면에서 얼마나 줄어들어 보이는지를 재서 줄여 둔 그림 가운데 하나를 고릅니다
- 그 그림에서 이웃한 칸들을 읽어 정해 둔 비율로 섞습니다
대가는 읽기 한 번에 칸을 여러 개 읽는 것입니다. 줄여 둔 그림을 만들어 두는 만큼 메모리도 더 씁니다.
상세
서류 한 뭉치를 복사기에 올려 작게 줄여 뽑는 장면을 떠올려 봅시다. 많이 줄일수록 원본의 글자 여러 개가 한 점 안으로 몰려듭니다. 얼마나 줄일지, 몰린 자리를 진하게 찍을지 연하게 찍을지는 서류가 아니라 복사기 쪽 손잡이로 미리 맞춰 둡니다.
샘플러는 텍스처에서 값을 읽는 규칙을 한 벌로 묶어 둔 것입니다. 텍스처는 표면에 입힐 그림을 격자로 담아 둔 자원입니다. 그 격자의 칸 하나를 텍셀이라고 부릅니다. 이 편에서는 그냥 칸이라고 쓰겠습니다.
그림의 어느 지점을 읽을지 가리키는 값은 텍스처 좌표입니다. 그림의 한쪽 끝이 0, 반대쪽 끝이 1 입니다.
읽기를 시키는 쪽은 셰이더입니다. 셰이더는 GPU(Graphics Processing Unit, 그래픽 처리 장치)에서 도는 작은 프로그램입니다. 화면에 찍힐 점 하나마다 한 번씩 돌며 그 점에 칠할 색을 정합니다.
셰이더가 좌표를 건네고 색을 돌려받는 이 읽기를 샘플링이라고 합니다. 한 장면을 그리는 동안 이 읽기가 수백만 번 일어납니다. 샘플러가 하는 일은 이 읽기 한 번을 어떻게 처리할지 정해 두는 것입니다.
좌표는 사람이 모델의 꼭짓점에만 적어 둡니다. 삼각형 안쪽 점의 좌표는 꼭짓점 값을 거리에 따라 섞어서 그때그때 만듭니다. 이렇게 사이값을 만드는 일을 보간이라고 합니다.
삼각형을 화면의 점들로 쪼개는 단계는 래스터화입니다. 그렇게 나온 점 하나를 프래그먼트라고 부릅니다.
flowchart TD
V["사람이 꼭짓점에 적어 둔 좌표"] --> R["래스터화 · 삼각형을 화면의 점들로 쪼갠다"]
R --> F["프래그먼트 · 쪼개서 나온 점 하나"]
F --> I["보간 · 꼭짓점 좌표를 섞어 그 점의 좌표를 만든다"]
I --> S["셰이더가 그 좌표를 샘플러에 건넨다"]
좌표가 샘플러에 닿기까지의 차례입니다. 사람이 적는 것은 맨 위 한 칸뿐이고, 나머지는 그리는 동안 만들어집니다.
그래서 프래그먼트의 좌표는 칸 한가운데에 떨어지는 일이 드뭅니다. 거의 언제나 칸과 칸 사이 어딘가를 가리킵니다. 그 지점의 색은 그림에 없으니 무엇을 돌려줄지 누군가 정해 줘야 합니다.
정해 줄 것은 하나가 아닙니다. 좌표가 0 과 1 밖으로 나갔을 때, 그림이 화면에서 아주 작게 보일 때, 면이 비스듬히 누워 있을 때마다 답이 달라집니다. 이 답들을 셰이더 코드 안에서 그때그때 고르면 같은 결정이 셰이더마다 되풀이됩니다. 샘플러는 그 결정들을 한 덩이로 묶어 그리기 전에 만들어 두는 것입니다.
그림과 읽는 법을 갈라 두는 까닭
읽는 규칙을 그림 안에 같이 넣어 두는 설계도 생각할 수 있습니다. 그렇게 하면 같은 그림을 두 방식으로 읽고 싶을 때 같은 그림을 메모리에 한 벌 더 올려야 합니다. 그림은 메모리를 많이 차지하는 물건입니다. 이 복제는 비쌉니다.
둘을 갈라 두면 그림 쪽과 규칙 쪽을 따로 늘립니다. 그림 여러 장에 같은 규칙을 물릴 수도 있고, 한 장에 서로 다른 규칙을 번갈아 물릴 수도 있습니다.
flowchart TD
subgraph 그림["메모리에 올려 둔 그림 넷"]
T1["벽돌"]
T2["나뭇결"]
T3["잔디"]
T4["철판"]
end
subgraph 규칙["메모리에 올려 둔 샘플러 셋"]
S1["부드럽게 섞고 되풀이한다"]
S2["각지게 읽고 되풀이한다"]
S3["부드럽게 섞고 끝에서 멈춘다"]
end
그림 --> P["읽을 때 그림 하나와 규칙 하나를 짝짓는다 · 짝은 열두 가지"]
규칙 --> P
메모리에 올려 둔 물건은 그림 넷과 샘플러 셋을 합쳐 일곱입니다. 그런데 만들 수 있는 읽기는 열두 가지입니다. 한쪽만 하나 늘려도 짝은 곱으로 불어납니다.
좌표가 칸과 칸 사이를 가리킬 때
물어본 지점이 칸 사이에 떨어지면 답이 두 갈래입니다. 가장 가까운 칸 하나의 색을 그대로 돌려주거나, 둘러싼 이웃 칸들을 거리에 따라 섞어 돌려주는 것입니다. 이 고르기를 텍스처 필터링이라고 합니다.
아래는 칸 넷이 만나는 근처에 지점 하나가 떨어진 모습입니다.
block-beta
columns 3
A["칸 A"] space B["칸 B"]
space P(("지점 P · 칸 C 쪽으로 치우쳤다")) space
C["칸 C"] space D["칸 D"]
가장 가까운 칸을 고르는 길은 칸 C 하나만 읽습니다. 섞는 길은 칸 A·B·C·D 넷을 다 읽고 P 와의 거리에 따라 비율을 매깁니다.
| 방식 | 읽는 칸 수 | 화면에 나타나는 모습 |
|---|---|---|
| 가장 가까운 칸 | 하나 | 칸 경계가 각지게 드러납니다 |
| 이웃을 섞기 | 넷 | 칸 경계가 부드럽게 번집니다 |
표의 두 줄에 우열은 없습니다. 각진 칸 경계가 그림의 뜻인 도트 그림은 위쪽을 고르고, 사진에서 딴 벽·바닥 재질은 아래쪽을 고릅니다.
샘플러는 이 고르기를 두 번 적습니다. 그림이 화면에서 늘어날 때와 줄어들 때를 따로 정합니다. 같은 그림이라도 크게 늘릴 때와 작게 줄일 때 곤란해지는 대목이 다르기 때문입니다.
화면에서 작게 보일 때
그림이 화면에서 줄어들면 화면의 점 하나가 그림의 여러 칸을 덮습니다. 그런데 읽는 것은 한 지점뿐이라 나머지 칸은 건너뜁니다. 물체가 조금만 움직여도 건너뛰는 칸이 바뀌어 무늬가 지글거립니다.
이 현상을 에일리어싱이라고 합니다. 격자무늬나 줄무늬처럼 규칙적인 그림에서는 원본에 없던 물결 무늬로 나타납니다. 그것을 모아레라고 부릅니다.
줄여 둔 그림을 쌓아 두면 이 현상이 줄어듭니다. 원본을 각 변 절반 크기로 줄인 그림, 그것을 다시 절반으로 줄인 그림을 차례로 만듭니다. 이 사슬이 밉맵입니다. 사슬의 한 칸은 밉 레벨입니다.
flowchart TD
L0["레벨 0 · 원본 · 물체가 코앞일 때 고른다"] --> L1["레벨 1 · 각 변이 절반 · 조금 멀어졌을 때"]
L1 --> L2["레벨 2 · 다시 절반 · 더 멀어졌을 때"]
L2 --> L3["레벨 3 · 다시 절반 · 멀리 있을 때"]
멀어질수록 사슬의 아래쪽 레벨을 고릅니다. 그리는 동안 레벨이 바뀌므로 사슬 전체를 메모리에 올려 둡니다.
줄인 그림은 만들 때 이웃 칸들을 미리 섞어 넣은 것입니다. 그래서 건너뛴 칸의 색이 그 안에 이미 담겨 있습니다. 한 지점만 읽어도 주변이 함께 반영됩니다.
샘플러가 하는 일은 어느 레벨에서 읽을지 고르는 것입니다. 화면에서 줄어든 정도를 재서 그에 맞는 레벨을 고르고, 두 레벨 사이에 걸치면 양쪽에서 읽어 섞습니다. 레벨을 섞을지 말지도 샘플러에 적습니다.
밉맵을 두는 대가는 메모리입니다. 줄인 그림을 전부 합치면 원본의 삼분의 일쯤이 더 듭니다.
면이 비스듬히 누우면 레벨 하나로는 안 맞습니다. 화면의 점 하나가 덮는 자국의 모양이 달라지기 때문입니다.
block-beta columns 4 H1["정면일 때 · 자국이 네모다"]:4 a1["덮인 칸"] a2["덮인 칸"] space space a3["덮인 칸"] a4["덮인 칸"] space space H2["비스듬히 누웠을 때 · 자국이 한쪽으로 길쭉하다"]:4 b1["덮인 칸"] b2["덮인 칸"] b3["덮인 칸"] b4["덮인 칸"]
정면일 때는 자국이 네모라 레벨 하나가 들어맞습니다. 비스듬할 때는 가로로 많이 줄고 세로로 덜 줄어듭니다. 그래서 어느 레벨을 고르든 한쪽 축이 흐려집니다.
누운 방향을 따라 여러 번 읽어 섞는 방식이 이방성 필터링입니다. 몇 번까지 읽을지의 상한도 샘플러에 적습니다.
0 과 1 을 벗어난 좌표
좌표에 1 보다 큰 값이나 0 보다 작은 값을 넣어도 됩니다. 그러면 그림 한 장을 넘어서는 지점을 가리킵니다. 그때 어디를 읽을지 정하는 규칙이 래핑 모드입니다.
| 방식 | 좌표로 1.5 를 받으면 | 고르는 경우 |
|---|---|---|
| 되풀이 | 0.5 로 봅니다 | 벽돌·타일처럼 같은 무늬를 이어 붙일 때 |
| 가장자리 고정 | 1 로 봅니다 | 그림 끝 색이 반대쪽으로 번지면 안 될 때 |
| 거울 되풀이 | 0.5 를 좌우로 뒤집어 봅니다 | 이어 붙인 자국을 감출 때 |
| 테두리 색 | 미리 정해 둔 색을 돌려줍니다 | 그림 밖을 빈 곳으로 두고 싶을 때 |
되풀이를 고르면 작은 그림 한 장으로 넓은 벽을 덮을 수 있습니다. 좌표를 0 에서 4 까지 주면 그림이 가로로 네 번 반복됩니다.
가로축과 세로축에 서로 다른 방식을 줘도 됩니다. 벽돌 무늬를 가로로만 이어 붙이고 세로는 끝에서 멈추게 하는 식입니다.
여기까지가 샘플러가 적어 두는 읽기 규칙입니다. 한 번의 읽기가 이 규칙들을 어떤 차례로 거치는지 모으면 이렇게 됩니다.
flowchart TD
C["셰이더가 좌표 하나를 건넨다"] --> W["0 과 1 밖이면 래핑 규칙으로 되돌린다"]
W --> L["줄어든 정도를 재서 밉 레벨을 고른다"]
L --> T["그 레벨에서 이웃한 칸을 읽는다"]
T --> M["필터 규칙대로 섞어 색 하나로 만든다"]
M --> R["셰이더에 색을 돌려준다"]
가운데 네 단계는 셰이더가 아니라 샘플러가 정합니다.
값을 견주기만 하는 샘플러
색을 돌려받는 대신 견준 결과를 돌려받는 쓰임도 있습니다. 셰이더가 좌표와 함께 견줄 값 하나를 같이 건네면, 샘플러는 그림에서 읽은 값을 그 값과 견줘 0 이나 1 로 돌려줍니다. 견주는 방식도 샘플러에 적습니다.
이 쓰임이 흔한 곳은 그림자 맵입니다. 빛에서 본 거리 값을 그림에 담아 두고, 지금 그리는 점의 거리와 견줘 그늘인지 아닌지를 가립니다. 이웃 칸 여러 개를 각각 견주고 그 결과를 섞으면 그림자 경계가 부드러워집니다.
거리 값을 먼저 섞고 나서 견주면 이 일이 안 됩니다. 섞어서 나온 평균은 어느 칸에도 없던 거리입니다. 그것으로 가른 경계는 그늘의 참모습이 아닙니다. 그래서 칸마다 견주고 그 결과를 섞는 순서를 샘플러 쪽에 맡깁니다.
만들기와 꽂기
앞의 소절들이 하나씩 다룬 규칙을 모으면 샘플러 하나에 적히는 것은 이렇습니다.
| 샘플러에 적는 것 | 다룬 소절 |
|---|---|
| 늘어날 때 쓸 필터 · 줄어들 때 쓸 필터 | 좌표가 칸과 칸 사이를 가리킬 때 |
| 밉 레벨 고르기 · 레벨 사이 섞기 · 이방성 읽기 상한 | 화면에서 작게 보일 때 |
| 축마다 정하는 래핑 모드 · 테두리 색 | 0 과 1 을 벗어난 좌표 |
| 견주는 방식 | 값을 견주기만 하는 샘플러 |
샘플러는 그림과 따로 만듭니다. 만들 때 이 규칙들을 한 번에 적어 넣고, 그 뒤로는 고치지 않습니다. 고쳐야 하면 새로 하나 만듭니다.
미리 굳혀 두면 그리는 동안 할 일이 줄어듭니다. 규칙이 정해져 있으면 읽기를 맡는 회로를 그에 맞춰 준비해 둘 수 있습니다.
셰이더가 쓸 자원은 하나씩 꽂지 않고 몇 개를 한 묶음으로 만들어 건넵니다. 그 묶음에 들어가는 항목 하나하나가 디스크립터입니다. 묶음 자체를 부르는 이름은 규격마다 다릅니다. 디스크립터 세트라고 부르는 쪽이 있고 바인드 그룹이라고 부르는 쪽이 있습니다.
샘플러도 그림과 나란히 그 묶음에 들어갑니다. 셰이더 쪽에서 그림과 규칙을 어떻게 받는지는 규격마다 갈립니다.
아래는 널리 쓰이는 셰이더 언어인 GLSL(OpenGL Shading Language, 오픈지엘 셰이딩 언어)로 그림과 규칙을 한 이름으로 받아 읽는 최소한의 예입니다.
uniform sampler2D brick; // 그림 + 읽는 규칙
in vec2 uv; // (0.25, 0.75)
out vec4 color;
void main() {
color = texture(brick, uv); // (0.8, 0.2, 0.1, 1)
}
셰이더가 적은 것은 어디를 읽을지 한 줄뿐입니다. 이웃 칸을 몇 개 읽을지, 어느 밉 레벨에서 읽을지, 범위 밖 좌표를 어떻게 다룰지는 전부 brick 에 묶여 있는 규칙이 정합니다. 돌려받은 값 넷은 빨강·초록·파랑과 불투명도입니다.
샘플러를 거치지 않고 읽는 길
칸 번호를 직접 대서 그 칸의 값을 손대지 않고 가져오는 읽기도 있습니다. 좌표가 칸 사이에 떨어질 일이 없으니 섞을 것도, 레벨을 고를 것도, 범위 밖을 다룰 것도 없습니다. 그래서 이 읽기에는 샘플러가 필요 없습니다.
이 길을 고르는 경우는 그림을 그림이 아니라 표로 쓸 때입니다. 계산에 쓸 숫자를 칸에 담아 두었다면 이웃과 섞이는 순간 값이 망가집니다.
화면을 다 그린 뒤 한 번 더 훑어 손보는 처리(포스트 프로세싱)에서도 마찬가지입니다. 화면의 점과 그림의 칸이 하나씩 맞아떨어지므로 섞을 이유가 없습니다.
거꾸로 표면에 그림을 입히는 일에는 샘플러가 거의 늘 끼어듭니다. 물체가 가까워지고 멀어지고 비스듬히 눕는 동안 어떻게 읽어야 할지가 계속 달라지기 때문입니다.
관련 항목
샘플러가 값을 읽어 오는 그림 자원
텍스처 · 텍셀 · 밉맵 · 밉 레벨 · 큐브 맵 · 텍스처 배열 · 텍스처 아틀라스 · 노멀 맵
샘플러가 적어 두는 읽기 규칙
텍스처 필터링 · 쌍선형 필터링 · 삼선형 필터링 · 최근접 이웃 샘플링 · 이방성 필터링 · 래핑 모드 · 테두리 색 · 밉 레벨 바이어스 · 비교 샘플러
샘플러에 건넬 좌표를 만들어 내는 기하 데이터
텍스처 좌표 · 정점 · 정점 속성 · 메시 · 보간 · 무게중심 좌표 · UV 언래핑
좌표가 색으로 바뀌기까지 거치는 처리 단계
래스터화 · 프래그먼트 · 프래그먼트 셰이더 · 샘플링 · 셰이더 · 그래픽스 파이프라인
샘플러를 셰이더에 물려주는 자원 묶음
디스크립터 · 디스크립터 세트 · 바인드 그룹 · 이미지 뷰 · 파이프라인 레이아웃 · 유니폼
샘플링을 잘못했을 때 화면에 나타나는 현상
에일리어싱 · 모아레 · 텍스처 블리딩 · 텍스처 늘어남
샘플러를 규정하는 그래픽스 규격
OpenGL · Vulkan · Direct3D · WebGPU · WebGL · GLSL · HLSL
샘플러를 밑바탕으로 삼는 그리기 기법
그림자 맵 · 환경 매핑 · 범프 매핑 · 라이트맵 · 포스트 프로세싱
샘플러가 속하는 상위 분류
다른 이름: sampler · Sampler · 텍스처 샘플러 · texture sampler · 샘플러 객체