사전 스텐실 버퍼
개념

스텐실 버퍼

gabury1고친 사람 github-actions[bot]

스텐실 버퍼는 화면의 어느 칸에 그려도 되는지를 칸마다 표시해 둡니다. 구역을 먼저 표시해 두면 이어지는 그리기는 그 표시를 보고 구역 안에만 색을 칠합니다. 거울에 비친 장면이 거울 테두리 밖으로 번지지 않는 것이 이 덕분입니다.

쉽고 빠른 이해

스텐실 버퍼는 화면 칸마다 스텐실 값이라는 작은 수를 적어 둡니다. 이 수로 색을 칠할 칸을 고릅니다. 벽에 걸린 거울을 그린다면 거울 유리가 덮은 칸에만 1을 적습니다. 나머지 칸은 0으로 둡니다.

이게 없으면 거울 속 장면을 거울 모양대로 직접 잘라 내야 합니다. 거울 테두리가 둥글거나 장식이 많을수록 잘라 내는 계산이 불어납니다.

  1. 한 장을 그리기 전에 모든 칸을 0으로 채웁니다
  2. 거울 유리만 먼저 그립니다. 이때 색은 안 칠합니다. 덮인 칸에 1만 적습니다
  3. 거울 속 장면을 그립니다. 스텐실 값이 1인 칸에만 색이 들어갑니다

대가는 메모리와 그리기 횟수입니다. 화면 칸 수만큼 수를 더 들고 있어야 합니다. 구역을 표시하는 그리기도 한 번 더 붙습니다.

테두리도 거칩니다. 칸마다 검사 결과는 칠한다와 안 칠한다 둘뿐입니다. 그래서 구역의 테두리가 계단처럼 각집니다.

상세

이 절은 스텐실 버퍼가 어떤 메모리인지, 그 안의 수를 누가 언제 쓰고 읽는지를 봅니다. 거울 하나를 그리는 장면을 처음부터 끝까지 들고 가면서 봅니다.

끝에서는 이 수로 만드는 화면 효과 몇 가지와, 이 방식이 치르는 값을 봅니다.

이름이 빌려 온 도구

벽에 글자를 칠할 때 글자 모양으로 구멍을 낸 판을 대고 스프레이를 뿌립니다. 페인트는 구멍 난 곳에만 묻습니다. 판을 대 둔 덕에 스프레이를 아무렇게나 뿌려도 글자 모양이 흐트러지지 않습니다. 이 판이 스텐실입니다.

스텐실 버퍼가 화면 위에서 이 판 노릇을 합니다. 구멍의 모양은 칸마다 적힌 수가 정합니다. 스프레이는 그 뒤에 이어지는 그리기입니다.

화면 칸마다 붙는 작은 수

화면은 작은 칸이 격자로 늘어선 것입니다. 칸 하나가 픽셀 하나입니다. 가로 몇 칸 · 세로 몇 줄인지는 화면의 해상도가 정합니다.

칸마다 보일 색을 적어 둔 메모리가 색 버퍼입니다. 모니터에 나가는 것이 이 메모리입니다. 스텐실 버퍼는 색 버퍼와 같은 크기의 메모리를 하나 더 둔 것입니다. 칸마다 색 대신 작은 정수 하나를 적습니다.

이 정수가 스텐실 값입니다. 스텐실 값에는 정해진 뜻이 없습니다. 1을 거울 구역으로 쓸지, 2를 창문 구역으로 쓸지는 그리는 쪽이 정합니다.

아래는 거울 하나를 표시해 둔 스텐실 버퍼를 작게 줄여 그린 것입니다. 1이 적힌 칸들이 가운데에 둥근 거울 모양을 이룹니다. 이 모양이 스텐실 판의 구멍입니다.

block-beta
  columns 7
  a1["0"] a2["0"] a3["0"] a4["0"] a5["0"] a6["0"] a7["0"]
  b1["0"] b2["0"] b3["1"] b4["1"] b5["1"] b6["0"] b7["0"]
  c1["0"] c2["1"] c3["1"] c4["1"] c5["1"] c6["1"] c7["0"]
  d1["0"] d2["1"] d3["1"] d4["1"] d5["1"] d6["1"] d7["0"]
  e1["0"] e2["0"] e3["1"] e4["1"] e5["1"] e6["0"] e7["0"]
  f1["0"] f2["0"] f3["0"] f4["0"] f5["0"] f6["0"] f7["0"]

칸의 거리를 담는 메모리도 흔히 곁에 둡니다. 칸마다 지금 보이는 물체가 카메라에서 얼마나 떨어졌는지를 적는 깊이 버퍼입니다. 깊이 버퍼의 수는 거리라는 뜻이 정해져 있습니다. 스텐실 값은 그리는 쪽이 뜻을 붙인다는 점이 둘을 가릅니다.

이 세 메모리를 화면 한 장을 그리는 한 벌로 묶은 것이 프레임버퍼입니다.

flowchart TD
    subgraph FB["프레임버퍼 · 화면 한 장"]
        C["색 버퍼 · 칸마다 색"]
        D["깊이 버퍼 · 칸마다 거리"]
        S["스텐실 버퍼 · 칸마다 스텐실 값"]
    end
    FB --> M["모니터에는 색 버퍼만 나간다"]

세 메모리는 칸 번호로 맞물립니다. 색 버퍼의 셋째 줄 다섯째 칸과 스텐실 버퍼의 셋째 줄 다섯째 칸은 화면의 같은 점을 말합니다. 그래서 스텐실 값을 보고 내린 판단이 같은 번호의 색에 바로 이어집니다.

한 칸에 8비트

스텐실 버퍼의 칸 하나는 흔히 8비트입니다. 8비트면 0부터 255까지 256가지 수를 담습니다. 구역을 가르는 데는 이 정도면 넉넉합니다.

8비트를 한 수로 쓰지 않고 쪼개 쓰기도 합니다. 맨 아래 비트는 거울 구역으로, 그 위 비트는 창문 구역으로 나눠 둡니다. 그러면 구역 여럿이 한 메모리를 나눠 씁니다.

packet-beta
0-0: "거울"
1-1: "창문"
2-7: "남는 비트"

칸 하나를 이렇게 비트로 나눠 둔 모양입니다. 거울을 다룰 때는 거울 비트만 봅니다. 창문을 다룰 때는 창문 비트만 봅니다.

볼 비트만 남기고 나머지를 가리는 값이 마스크입니다. 거울 비트만 보려면 맨 아래 비트만 1인 마스크를 씌웁니다. 마스크에서 0인 비트는 칸에 무엇이 적혀 있든 보지 않습니다.

스텐실 버퍼는 깊이 버퍼와 한 덩이로 묶어 두는 일이 흔합니다. 깊이 24비트와 스텐실 8비트를 붙여 칸 하나를 32비트로 쓰는 형식이 대표적입니다.

packet-beta
0-23: "깊이"
24-31: "스텐실"

묶는 까닭은 두 값이 함께 읽히고 쓰이기 때문입니다. 떨어뜨려 두면 칸 하나를 다룰 때마다 메모리 두 군데를 다녀와야 합니다. 둘을 한 칸에 담는 이 방식이 깊이 스텐실 형식입니다.

스텐실 값을 읽는 검사

스텐실 버퍼는 스스로 아무것도 판단하지 않습니다. 수를 담아 둘 뿐입니다. 판단은 그 수를 읽는 다른 단계가 맡습니다.

판단을 받는 것은 물체가 아니라 칸 하나를 칠하겠다고 나선 후보 하나입니다. 물체 하나를 그리면 그 물체가 덮는 칸마다 후보가 하나씩 생깁니다. 이 후보가 프래그먼트입니다.

프래그먼트가 칸에 도착하면 그 칸의 스텐실 값을 읽어 미리 정해 둔 수와 견줍니다. 이 견주기가 스텐실 테스트입니다. 미리 정해 두는 수를 참조값이라 합니다. 거울 구역을 1로 표시해 두었다면 참조값을 1로 두고 스텐실 값이 1과 같은 칸만 통과시킵니다.

견주기 전에 두 수 모두에 마스크를 씌웁니다. 칸의 스텐실 값과 참조값에서 볼 비트만 남긴 뒤 견줍니다. 앞 절에서 거울 비트만 본다고 한 것이 이 단계에서 일어납니다.

견주는 규칙은 고를 수 있습니다. 같을 때 · 다를 때 · 클 때 · 작을 때 통과시키는 규칙이 있습니다. 언제나 통과시키거나 언제나 떨어뜨리는 규칙도 있습니다.

크고 작음을 따질 때 왼쪽에 오는 것은 참조값입니다. 「작을 때」를 고르면 참조값이 칸의 스텐실 값보다 작은 칸이 통과합니다.

이 견주기는 화면을 그리는 전용 장치인 GPU(Graphics Processing Unit, 그래픽 처리 장치)가 맡습니다. 칸이 수백만 개라도 칸마다 한꺼번에 돌립니다.

통과한 프래그먼트는 깊이 테스트로 넘어갑니다. 깊이 테스트는 칸에 적힌 거리보다 프래그먼트가 가까운지를 보고 가까운 것만 남기는 검사입니다. 둘 다 통과해야 그 칸에 색이 들어갑니다.

스텐실 값을 고쳐 쓰는 규칙

스텐실 값은 한 장을 그리는 사이에 바뀝니다. 바꾸는 쪽도 검사입니다. 프래그먼트가 검사를 거친 결과에 따라 그 칸의 스텐실 값을 어떻게 고칠지 미리 정해 둡니다.

결과는 세 갈래입니다. 스텐실 테스트에서 떨어진 경우, 스텐실 테스트를 지나 깊이 테스트에서 떨어진 경우, 둘 다 통과한 경우입니다.

flowchart TD
    F["프래그먼트"] --> ST{"스텐실 테스트"}
    ST -->|탈락| B1["갈래 1 · 스텐실 값을 고친다 · 버린다"]
    ST -->|통과| DT{"깊이 테스트"}
    DT -->|탈락| B2["갈래 2 · 스텐실 값을 고친다 · 버린다"]
    DT -->|통과| B3["갈래 3 · 스텐실 값을 고친다 · 색을 쓴다"]

그림은 프래그먼트 하나가 두 검사를 거쳐 세 갈래 중 하나에 닿는 길입니다. 어느 갈래에서든 스텐실 값을 고칠 수 있습니다. 색이 들어가는 것은 셋째 갈래뿐입니다.

갈래마다 아래 여섯 방법에서 하나를 고릅니다.

고치는 방법 스텐실 값이 어떻게 되나
그대로 둔다 안 바뀐다
참조값으로 바꾼다 참조값이 된다
0으로 지운다 0이 된다
1 늘린다 1 커진다. 255에서는 멈추거나 0으로 돌아간다
1 줄인다 1 작아진다. 0에서는 멈추거나 255로 돌아간다
비트를 뒤집는다 0인 비트는 1로, 1인 비트는 0으로

이 규칙 덕에 검사에서 떨어진 프래그먼트도 흔적을 남깁니다. 화면에는 아무 색도 안 남습니다. 그래도 그 칸의 스텐실 값에는 프래그먼트가 다녀갔다는 기록이 남습니다.

고칠 수 있는 비트도 막아 둘 수 있습니다. 고쳐도 되는 비트만 고르는 값이 쓰기 마스크입니다. 거울 비트를 고치는 동안 창문 비트가 덩달아 바뀌지 않게 막는 것이 이 값입니다.

아래는 거울 한 장을 그리는 동안 거울 유리 안쪽 칸 하나의 스텐실 값이 겪는 일입니다.

stateDiagram-v2
    state "스텐실 값 0" as S0
    state "스텐실 값 1" as S1
    [*] --> S0: 한 장을 시작하며 전부 0으로 지운다
    S0 --> S1: 거울 유리가 이 칸을 덮는다 · 참조값인 1을 적는다
    S1 --> S1: 거울 속 장면이 이 칸을 칠한다 · 그대로 둔다
    S1 --> S0: 다음 장을 시작하며 다시 지운다

그림의 첫 단계는 지우기입니다. 지난 장의 스텐실 값이 남아 있으면 이번 장에서 거울이 없는 칸에도 거울 속 장면이 새어 나옵니다. 그래서 한 장마다 모든 칸을 0으로 되돌립니다.

거울 하나를 그리는 두 번의 그리기

거울 장면은 같은 화면을 두 번 그려서 만듭니다. 첫 번째는 구역을 표시하는 그리기입니다. 두 번째는 그 구역을 채우는 그리기입니다.

첫 그리기에서는 색 쓰기를 꺼 둡니다. 거울 유리 모양만 그립니다. 그 모양이 덮은 칸의 스텐실 값을 1로 바꿉니다. 화면의 색은 하나도 안 바뀝니다.

둘째 그리기에서는 거울 속 장면을 그립니다. 이번에는 스텐실 값이 1인 칸만 통과시킵니다. 거울 밖으로 나가려던 프래그먼트는 검사에서 떨어집니다.

두 그리기는 서로를 몰라도 맞물립니다. 거울 테두리가 아무리 복잡해도 둘째 그리기는 칸의 스텐실 값만 봅니다. 거울 속에 무엇이 비치든 첫 그리기는 달라지지 않습니다.

스텐실 버퍼가 없으면 거울 속 장면을 거울 모양대로 직접 잘라 내야 합니다. 그릴 물체마다 거울 테두리와 겹치는 부분을 계산해 잘라 내는 것입니다. 테두리가 둥글거나 장식이 많을수록 잘라 내는 계산이 불어납니다.

카운터로 쓰기

스텐실 값은 1씩 늘리고 줄일 수 있습니다. 그래서 스텐실 버퍼는 구역 표시 말고 카운터로도 쓰입니다. 칸마다 무언가가 몇 번 지나갔는지를 세는 것입니다.

그 쓰임의 대표가 그림자 볼륨입니다. 빛을 가린 물체 뒤로는 그늘이 뻗는 공간이 생깁니다. 이 공간을 기둥 모양의 물체로 세워 두는 것이 그림자 볼륨입니다. 칸에 보이는 물체의 점이 이 기둥 안에 있으면 그 점은 그늘에 든 것입니다.

그 점이 기둥 안인지는 면을 세어 알아냅니다. 칸 하나에서 카메라가 바라보는 방향으로 선을 그어 봅니다. 이 시선은 칸에 보이는 물체의 점에서 끝납니다. 그 사이에 기둥의 면을 몇 번 지나는지를 셉니다.

기둥의 면은 카메라를 향한 앞면과 카메라를 등진 뒷면으로 나뉩니다. 앞면이 칸을 지나가면 스텐실 값을 1 늘립니다. 뒷면이 지나가면 1 줄입니다.

셀 때는 보이는 물체보다 카메라 쪽에 있는 면만 셉니다. 물체 뒤에 숨은 면은 깊이 테스트에서 떨어지기 때문입니다. 앞 절의 셋째 갈래, 곧 두 검사를 다 통과한 경우에만 늘리고 줄이게 정해 둡니다.

아래는 칸 셋에서 셈이 어떻게 남는지를 보인 것입니다.

flowchart TD
    subgraph P1["칸 ① 보이는 점이 기둥 뒤에 있다"]
        A1["앞면 +1"] --> A2["뒷면 −1"] --> A3["0 · 밝다"]
    end
    subgraph P2["칸 ② 보이는 점이 기둥 안에 있다"]
        B1["앞면 +1"] --> B2["뒷면은 보이는 점 뒤라 안 센다"] --> B3["1 · 그늘"]
    end
    subgraph P3["칸 ③ 시선이 기둥을 비껴간다"]
        C1["지나는 면이 없다"] --> C2["0 · 밝다"]
    end
    A3 ~~~ B1
    B3 ~~~ C1

다 세고 나서 0이 아닌 칸은 그 칸에 보이는 물체의 점이 기둥 안에 있는 칸입니다. 곧 그늘이 진 칸입니다. 그 칸들만 어둡게 칠하면 그림자가 됩니다.

카운터로 쓸 때는 8비트의 끝을 신경 써야 합니다. 1 늘리기와 1 줄이기에는 두 가지가 있습니다. 끝에 닿으면 멈추는 쪽과 반대쪽 끝으로 돌아가는 쪽입니다.

그림자 볼륨에서는 이 차이가 드러납니다. 기둥의 면을 그리는 순서에 따라 뒷면이 앞면보다 먼저 칸을 지날 수 있습니다. 그러면 스텐실 값이 0인 칸에서 1을 줄이게 됩니다.

flowchart TD
    subgraph W1["멈추는 쪽"]
        a0["0"] -->|뒷면 −1| a1["0 · 끝에 막힌다"] -->|앞면 +1| a2["1 · 그늘로 잘못 칠한다"]
    end
    subgraph W2["돌아가는 쪽"]
        b0["0"] -->|뒷면 −1| b1["255"] -->|앞면 +1| b2["0 · 밝다"]
    end
    a2 ~~~ b0

그림은 칸 ①과 같은 칸에서 뒷면이 먼저 온 경우입니다. 멈추는 쪽은 0에 막혀 원래 수로 못 돌아옵니다. 그래서 늘리기와 줄이기가 섞이는 셈에서는 돌아가는 쪽을 고릅니다.

스텐실 버퍼로 만드는 화면 효과

구역을 무엇으로 정하느냐에 따라 만드는 효과가 갈립니다.

효과 색이 들어가는 구역
거울 · 차원문 거울 유리나 문틀이 덮은 칸
오려 낸 창 창이나 말풍선 테두리가 덮은 칸
그림자 볼륨 그림자 기둥의 앞뒤 면을 세고 남은 칸
벽에 붙이는 무늬 무늬를 붙일 벽면이 덮은 칸
물체 윤곽선 물체가 덮은 칸을 뺀 나머지

윤곽선은 구역을 거꾸로 씁니다. 물체를 먼저 그립니다. 이때 물체가 덮은 칸에 1을 적습니다.

그다음 같은 물체를 조금 키워 다시 그립니다. 이번에는 스텐실 값이 1이 아닌 칸만 통과시킵니다. 원래 모양 둘레에만 색이 남아 테두리가 됩니다.

치르는 값

첫째는 메모리입니다. 가로 1920 · 세로 1080 화면이면 칸이 200만 개쯤 됩니다. 칸마다 8비트면 2메가바이트쯤 듭니다.

깊이 버퍼와 한 칸에 묶어도 이 8비트는 그대로 듭니다. 묶어서 줄어드는 것은 메모리를 오가는 횟수입니다.

둘째는 그리기 횟수입니다. 구역을 표시하는 그리기가 한 번 더 붙습니다. 한 장마다 모든 칸을 지우는 일도 붙습니다.

셋째는 가장자리입니다. 스텐실 테스트의 답은 통과 아니면 탈락뿐입니다. 절반만 칠해 테두리를 부드럽게 만드는 일은 이 검사로 안 됩니다. 그래서 비스듬한 테두리가 칸 단위로 각지는 에일리어싱이 생깁니다.

넷째는 스텐실 값이 화면 칸에 매여 있다는 점입니다. 카메라가 움직이면 거울이 덮는 칸도 바뀝니다. 그래서 한 번 표시해 둔 구역을 다음 장에 다시 쓰지 못합니다. 장마다 새로 표시합니다.

구역이 곧은 사각형이면 스텐실 버퍼까지 안 가도 됩니다. 화면의 사각형 범위 밖을 잘라 내는 시저 테스트가 있습니다. 그쪽은 메모리도 첫 그리기도 필요 없습니다. 스텐실 버퍼는 사각형으로 나타낼 수 없는 구역을 맡습니다.

관련 항목

스텐실 버퍼와 한 벌로 묶이는 화면 메모리

프레임버퍼 · 색 버퍼 · 깊이 버퍼 · 렌더 타깃 · 프레임버퍼 객체 · 누적 버퍼 · G 버퍼

스텐실 버퍼의 칸을 이루는 값과 형식

비트 · 비트 마스크 · 깊이 스텐실 형식 · 픽셀 포맷 · 픽셀 · 해상도

스텐실 버퍼를 읽고 고쳐 쓰는 처리 단계

스텐실 테스트 · 깊이 테스트 · 프래그먼트별 테스트 · 출력 병합기 · 프래그먼트 셰이더 · 래스터화 · 그래픽스 파이프라인 · 프래그먼트

스텐실 값을 견주고 고치는 설정값

스텐실 참조값 · 스텐실 마스크 · 스텐실 쓰기 마스크 · 스텐실 연산 · 비교 함수 · 색 쓰기 마스크

스텐실 버퍼로 만드는 화면 효과

그림자 볼륨 · 거울 반사 · 포털 렌더링 · 데칼 · 윤곽선 그리기 · 클리핑 마스크

스텐실 버퍼 대신 구역을 거는 수단

시저 테스트 · 클립 평면 · 알파 마스킹 · 프래그먼트 버리기 · 알파 테스트

스텐실 버퍼의 가장자리 문제와 그 대처

에일리어싱 · 안티에일리어싱 · 멀티샘플링 · 계단 현상

화면을 여러 번 그려 버퍼를 쌓는 기법

멀티패스 렌더링 · 디퍼드 셰이딩 · Z 프리패스 · 오버드로

스텐실 버퍼를 다루는 장치와 그래픽 API

GPU · OpenGL · Vulkan · Direct3D · Metal · WebGL · WebGPU

스텐실 버퍼가 속하는 상위 분류

그래픽스 · 렌더링 · 실시간 렌더링 · 래스터 그래픽스 · 버퍼

다른 이름: stencil buffer · 스텐실 메모리