Skip to content

스텐실 테스트

프래그먼트 셰이더가 프래그먼트 처리를 마치면 스텐실 테스트라는 것이 실행되는데, 깊이 테스트와 마찬가지로 프래그먼트를 버릴 수 있는 옵션이 있습니다. 그 후 남은 프래그먼트들은 깊이 테스트로 전달되고, OpenGL은 여기서 추가적인 프래그먼트를 버릴 수도 있습니다. 스텐실 테스트는 스텐실 버퍼라는 또 다른 버퍼의 내용을 기반으로 하는데, 이 버퍼는 렌더링 중에 업데이트하여 흥미로운 효과를 구현할 수 있습니다.

스텐실 버퍼는 일반적으로 스텐실 값당 8비트를 포함하며, 이는 픽셀당 총 256개의 서로 다른 스텐실 값을 저장할 수 있음을 의미합니다. 이러한 스텐실 값은 원하는 값으로 설정할 수 있으며, 특정 프래그먼트가 특정 스텐실 값을 가질 때 해당 프래그먼트를 유지하거나 폐기할 수 있습니다.

각 윈도우 라이브러리는 스텐실 버퍼를 설정해야 합니다. GLFW는 이를 자동으로 수행하므로 GLFW에 스텐실 버퍼 생성을 지시할 필요가 없지만, 다른 윈도우 라이브러리는 기본적으로 스텐실 버퍼를 생성하지 않을 수 있으므로 해당 라이브러리의 문서를 확인하세요.

아래는 간단한 스텐실 버퍼의 예입니다(픽셀 크기는 실제와 다릅니다).

스텐실 버퍼는 먼저 0으로 초기화된 다음, 1로 채워진 빈 사각형 영역이 스텐실 버퍼에 저장됩니다. 그런 다음 장면의 프래그먼트는 해당 프래그먼트의 스텐실 값에 1이 포함된 부분에 대해서만 렌더링되고 나머지는 버려집니다.

스텐실 버퍼 작업을 통해 프래그먼트를 렌더링하는 모든 위치에서 스텐실 버퍼를 특정 값으로 설정할 수 있습니다. 렌더링 중에 스텐실 버퍼의 내용을 변경하는 것은 스텐실 버퍼에 쓰는 것과 같습니다. 같은 프레임(또는 다음 프레임)에서 이러한 값을 읽어 특정 프래그먼트를 폐기하거나 통과시킬 수 있습니다. 스텐실 버퍼를 사용하면 원하는 만큼 복잡한 구조를 만들 수 있지만, 일반적인 개요는 다음과 같습니다.

  • 스텐실 버퍼에 쓰기 기능을 활성화합니다.
  • 객체를 렌더링하고 스텐실 버퍼의 내용을 업데이트합니다.
  • 스텐실 버퍼에 쓰기 기능을 비활성화합니다.
  • 이번에는 스텐실 버퍼의 내용에 따라 특정 프래그먼트를 버리면서 (다른) 객체를 렌더링합니다.

스텐실 버퍼를 사용하면 장면에 그려진 다른 객체의 프래그먼트를 기반으로 특정 프래그먼트를 버릴 수 있습니다.

GL_STENCIL_TEST를 활성화하면 스텐실 테스트를 사용할 수 있습니다. 이후 모든 렌더링 호출은 어떤 방식으로든 스텐실 버퍼에 영향을 미칩니다.

glEnable(GL_STENCIL_TEST);

색상 및 깊이 버퍼와 마찬가지로 스텐실 버퍼도 매 반복마다 지워야 한다는 점에 유의하십시오.

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 

또한, 깊이 테스트의 glDepthMask 함수와 마찬가지로 스텐실 버퍼에도 동일한 기능을 하는 함수가 있습니다. glStencilMask 함수를 사용하면 버퍼에 기록될 스텐실 값과 AND 연산될 비트마스크를 설정할 수 있습니다. 기본값은 모든 비트가 1로 설정되어 출력에 영향을 주지 않지만, 이 값을 0x00으로 설정하면 버퍼에 기록되는 모든 스텐실 값이 0이 됩니다. 이는 깊이 테스트의 glDepthMask(GL_FALSE)와 동일한 기능을 합니다.

glStencilMask(0xFF); // 각 비트는 그대로 스텐실 버퍼에 기록됩니다.
glStencilMask(0x00); // 각 비트는 스텐실 버퍼에서 0으로 처리됩니다(쓰기 비활성화).

대부분의 경우 스텐실 마스크로 0x00 또는 0xFF만 사용하게 되지만, 사용자 지정 비트 마스크를 설정할 수 있는 옵션이 있다는 것을 알아두면 좋습니다.

스텐실 기능

심층 테스트와 마찬가지로 스텐실 테스트의 통과 또는 실패 시점과 스텐실 버퍼에 미치는 영향을 어느 정도 제어할 수 있습니다. 스텐실 테스트를 구성하는 데 사용할 수 있는 함수는 glStencilFunc와 glStencilOp 두 가지입니다.

glStencilFunc(GLenum func, GLint ref, GLuint mask) 함수는 세 개의 매개변수를 가집니다.

  • func: 프래그먼트가 테스트를 통과할지 또는 폐기될지를 결정하는 스텐실 테스트 함수를 설정합니다. 이 테스트 함수는 저장된 스텐실 값과 glStencilFunc의 참조 값에 적용됩니다. 가능한 옵션은 GL_NEVER, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL, GL_EQUAL, GL_NOTEQUAL 및 GL_ALWAYS입니다. 이러한 옵션의 의미는 깊이 버퍼의 함수와 유사합니다.
  • ref: 스텐실 테스트의 기준값을 지정합니다. 스텐실 버퍼의 내용은 이 값과 비교됩니다.
  • mask: 테스트에서 기준값과 저장된 스텐실 값을 비교하기 전에 두 값 모두와 AND 연산될 마스크를 지정합니다. 초기값은 모두 1입니다.

처음에 보여드린 간단한 스텐실 예시의 경우, 함수는 다음과 같이 설정됩니다.

glStencilFunc(GL_EQUAL, 1, 0xFF);

이는 OpenGL에게 프래그먼트의 스텐실 값이 참조 값 1과 같을 때(GL_EQUAL) 해당 프래그먼트가 테스트를 통과하여 그려지고, 그렇지 않으면 폐기된다는 것을 알려줍니다.

하지만 glStencilFunc는 스텐실 버퍼의 내용에 따라 OpenGL이 프래그먼트를 통과시킬지 버릴지만 결정할 뿐, 실제로 버퍼를 어떻게 업데이트하는지는 설명하지 않습니다. 바로 이 부분에서 glStencilOp가 필요합니다.

glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)에는 세 가지 옵션이 있으며, 각 옵션에 대해 수행할 작업을 지정할 수 있습니다.

  • sfail: 스텐실 테스트가 실패할 경우 취할 조치.
  • dpfail: 스텐실 테스트는 통과했지만 깊이 테스트가 실패했을 경우 취할 조치.
  • dppass: 스텐실 테스트와 깊이 테스트가 모두 통과했을 경우 취할 조치.

각 옵션에 대해 다음과 같은 조치를 취할 수 있습니다.

행동 설명
GL_KEEP 현재 저장된 스텐실 값이 유지됩니다.
GL_ZERO 스텐실 값을 0으로 설정합니다.
GL_REPLACE 스텐실 값은 glStencilFunc로 설정된 참조 값으로 대체됩니다.
GL_INCR 스텐실 값이 최대값보다 낮으면 1만큼 증가합니다.
GL_INCR_WRAP GL_INCR과 동일하지만, 최대값을 초과하는 즉시 0으로 되돌아갑니다.
GL_DECR 스텐실 값이 최소값보다 높으면 1만큼 감소합니다.
GL_DECR_WRAP GL_DECR과 동일하지만, 값이 0보다 작을 경우 최대값으로 래핑됩니다.
GL_INVERT 현재 스텐실 버퍼 값을 비트 단위로 반전합니다.

glStencilOp 함수는 기본적으로 (GL_KEEP, GL_KEEP, GL_KEEP)로 설정되어 있으므로 테스트 결과와 관계없이 스텐실 버퍼의 값은 그대로 유지됩니다. 기본 동작은 스텐실 버퍼를 업데이트하지 않으므로 스텐실 버퍼에 값을 쓰려면 옵션 중 하나 이상에 대해 다른 동작을 지정해야 합니다.

따라서 glStencilFunc와 glStencilOp를 사용하면 스텐실 버퍼를 언제, 어떻게 업데이트할지, 그리고 내용에 따라 프래그먼트를 언제 통과시키거나 버릴지 정확하게 지정할 수 있습니다.

객체 외곽선

이전 섹션만으로는 스텐실 테스트의 작동 방식을 완전히 이해하기 어려울 수 있으므로, 스텐실 테스트만으로 구현할 수 있는 유용한 기능 중 하나인 객체 외곽선 기능을 시연해 보이겠습니다.

객체 외곽선 기능은 이름 그대로 각 객체(또는 하나만)에 대해 (결합된) 객체 주위에 작은 색상 테두리를 만듭니다. 이 기능은 전략 게임에서 유닛을 선택하고 사용자에게 어떤 유닛이 선택되었는지 보여줘야 할 때 특히 유용합니다. 객체 외곽선을 만드는 루틴은 다음과 같습니다.

  • 스텐실 쓰기를 활성화하세요.
  • (외곽선을 그릴) 객체를 그리기 전에 스텐실 연산을 GL_ALWAYS로 설정합니다. 그러면 객체의 프래그먼트가 렌더링되는 모든 위치에서 스텐실 버퍼를 1로 업데이트됩니다.
  • 객체를 렌더링합니다.
  • 스텐실 쓰기 및 깊이 테스트를 비활성화합니다.
  • 각 개체의 크기를 조금씩 확대하세요.
  • 단일 (테두리) 색상을 출력하는 다른 프래그먼트 셰이더를 사용하십시오.
  • 해당 개체를 다시 그리되, 개체 프래그먼트의 스텐실 값이 1과 같지 않은 경우에만 다시 그립니다.
  • 깊이 테스트를 다시 활성화하고 스텐실 기능을 GL_KEEP으로 복원합니다.

이 과정은 객체의 각 프래그먼트에 대해 스텐실 버퍼의 내용을 1초 단위로 설정하고, 테두리를 그릴 시점이 되면 스텐실 테스트를 통과한 부분에 대해서만 확대된 객체 버전을 그립니다. 즉, 스텐실 버퍼를 사용하는 경우, 확대된 버전의 객체 중 원래 객체의 프래그먼트에 해당하는 부분은 모두 버리는 것입니다.

먼저 테두리 색상을 출력하는 아주 기본적인 프래그먼트 셰이더를 만들어 보겠습니다. 간단하게 하드코딩된 색상 값을 설정하고 셰이더 이름을 shaderSingleColor로 지정합니다.

void main()
{
    FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}

이전 장의 장면을 사용하여 두 컨테이너에 객체 외곽선을 추가할 것이므로 바닥은 제외하겠습니다. 먼저 바닥을 그리고, 그 다음 두 컨테이너를 (스텐실 버퍼에 쓰면서) 그린 후, 확대된 컨테이너를 그립니다 (이전에 그려진 컨테이너 프래그먼트를 덮어쓰는 프래그먼트는 버립니다).

먼저 스텐실 테스트를 활성화해야 합니다.

glEnable(GL_STENCIL_TEST);
````

그리고 각 프레임에서 스텐실 테스트가 성공하거나 실패할 때마다 수행할 작업을 지정하려고 합니다.

```c++
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  

테스트 중 하나라도 실패하면 아무 작업도 수행하지 않고 스텐실 버퍼에 현재 저장된 값을 그대로 유지합니다. 하지만 스텐실 테스트와 깊이 테스트 모두 성공하면 저장된 스텐실 값을 glStencilFunc를 통해 설정한 참조 값(나중에 1로 설정)으로 대체합니다.

프레임 시작 시 스텐실 버퍼를 0으로 초기화하고, 컨테이너의 경우 그려진 각 프래그먼트마다 스텐실 버퍼를 1로 업데이트합니다.

glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 모든 프래그먼트는 스텐실 테스트를 통과해야 합니다.
glStencilMask(0xFF); // 스텐실 버퍼에 쓰기 활성화
normalShader.use();
DrawTwoContainers();

GL_REPLACE를 스텐실 연산 함수로 사용함으로써 각 컨테이너의 프래그먼트가 스텐실 버퍼를 스텐실 값 1로 업데이트하도록 합니다. 프래그먼트는 항상 스텐실 테스트를 통과하므로, 프래그먼트가 그려진 모든 위치에서 스텐실 버퍼가 참조 값으로 업데이트됩니다.

컨테이너가 그려진 위치에 1초 간격으로 스텐실 버퍼가 업데이트되었으므로, 이제 확대된 컨테이너를 그리겠지만, 이번에는 적절한 테스트 함수를 사용하고 스텐실 버퍼에 대한 쓰기를 비활성화할 것입니다.

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 스텐실 버퍼에 쓰기 기능을 비활성화합니다.
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use(); 
DrawTwoScaledUpContainers();

컨테이너의 스텐실 값이 1이 아닌 부분만 그리도록 스텐실 함수를 GL_NOTEQUAL로 설정했습니다. 이렇게 하면 이전에 그려진 컨테이너 바깥쪽 부분만 그려집니다. 또한, 확대된 컨테이너(예: 테두리)가 바닥에 덮어쓰여지지 않도록 깊이 테스트를 비활성화했습니다. 작업이 완료되면 깊이 테스트를 다시 활성화해야 합니다.

우리 장면의 전체 객체 외곽선 그리기 루틴은 다음과 같습니다.

glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 

glStencilMask(0x00); // 바닥을 그리는 동안 스텐실 버퍼가 업데이트되지 않도록 해야 합니다.
normalShader.use();
DrawFloor()  

glStencilFunc(GL_ALWAYS, 1, 0xFF); 
glStencilMask(0xFF); 
DrawTwoContainers();

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); 
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use(); 
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 1, 0xFF);   
glEnable(GL_DEPTH_TEST); 

스텐실 테스트의 기본 개념을 이해하고 있다면 이 내용도 어렵지 않을 것입니다. 그렇지 않다면 이전 섹션을 다시 한번 꼼꼼히 읽고, 예시를 통해 각 기능이 실제로 어떤 역할을 하는지 완전히 이해해 보세요.

외곽선 알고리즘의 결과는 다음과 같습니다.

객체 외곽선 알고리즘의 전체 코드는 여기에서 확인하세요.

보시다시피 두 컨테이너의 테두리가 겹치는데, 이는 일반적으로 우리가 원하는 효과입니다(10개의 유닛을 선택해야 하는 전략 게임을 생각해 보세요. 테두리가 겹치는 것이 일반적으로 선호됩니다). 객체별로 완전한 테두리를 원한다면 객체별로 스텐실 버퍼를 지우고 깊이 버퍼를 약간 조정해야 합니다.

앞서 보신 객체 외곽선 알고리즘은 게임에서 선택된 객체를 시각화하는 데 흔히 사용됩니다(전략 게임을 생각해 보세요). 이러한 알고리즘은 모델 클래스 내에서 쉽게 구현할 수 있습니다. 모델 클래스에 부울 플래그를 설정하여 테두리를 표시할지 여부를 지정할 수 있습니다. 좀 더 창의적인 디자인을 원한다면 가우시안 블러와 같은 후처리 필터를 사용하여 테두리를 더욱 자연스럽게 표현할 수도 있습니다.

스텐실 테스트는 (객체 외곽선 그리기 외에도) 백미러 내부에 텍스처를 그려 거울 모양에 깔끔하게 맞추거나, 섀도우 볼륨이라는 스텐실 버퍼 기술을 사용하여 실시간 그림자를 렌더링하는 등 훨씬 더 많은 용도로 사용됩니다. 스텐실 버퍼는 이미 방대한 OpenGL 툴킷에 또 하나의 유용한 도구를 더해줍니다.

Comments