OpenGL

[Learn OpenGL 번역] 5-2. 고급 OpenGL - Stencil testing

짱승_ 2018. 8. 5. 17:15

Stencil testing

고급 OpenGL/Stencil-testing

  Fragment shader가 fragment를 처리하고 나면 depth test와 비슷한 stencil test라고 불리는 것이 수행됩니다. fragment가 폐기될지 안될지 테스트하는 것입니다. 그런 후 남아있는 fragment는 dpeth test로 보내져 폐기될지 안될지를 또 한번 테스트됩니다. 이 stencil test는 지금까지의 buffer와는 다른 stencil buffer라고 불리는 buffer를 기반으로 수행됩니다. 이 buffer는 흥미로운 효과를 내기위해 렌더링 동안에 수정할 수 있습니다.


  Stencil buffer는 (일반적으로) 8 비트의 stencil value를 가지고 있고 이 값은 pixel/fragment마다 256개의 값으로 나타내어집니다. 우리는 이 stencil 값을 설정하여 특정한 stencil 값을 가지고 있는 특정 fragment를 폐기할지 유지할지를 정할 수 있습니다.

  각 window 라이브러리들은 stencil buffer를 세팅해야만 작동합니다. GLFW는 이를 자동적으로 해주기때문에 우리는 GLFW에게 생성하라고 지시할 필요가 없습니다. 하지만 다른 window 라이브러리들은 기본값으로 stencil을 생성하지 않을수도 있으므로 라이브러리 문서를 확인해야합니다.

  Stencil buffer의 간단한 예제는 다음과 같습니다.


A simple demonstration of a stencil buffer

  이 stencil buffer는 먼저 0으로 채워지고나서 속이 비어있는 사각형 모양의 1을 설정합니다. 그러면 이 scene의 fragment들 중에서 stencil 값이 1인 fragment들만 렌더링됩니다(다른 것들은 폐기됩니다).


  Stencil buffer는 우리가 fragment를 렌더링해야할 곳에 특정 값을 설정할 수 있도록 합니다. 렌더링하는 도중에 stencil buffer를 수정함으로써 우리는 stencil buffer를 writing(작성)합니다. 동일한 렌더링 루프에서 우리는 특정 fragment들을 폐기하거나 유지하기위해서 이 값들을 read(읽다)할 수 있습니다. stencil buffer를 여러분 마음대로 사용할 수 있지만 다음과 같은 일반적인 틀이 있습니다.


  • stencil buffer 작성 활성화
  • 오브젝트 렌더링, stencil buffer 수정
  • stencil buffer 작성 비활성화
  • stencil buffer를 기반으로 특정 fragment를 폐기하여 오브젝트 렌더링

  Stencil buffer를 사용함으로써 scene에 그려진 다른 오브젝트의 fragment들을 기반으로하여 특정 fragment를 폐기시킬 수 있습니다.


  GL_STENCIL_TEST를 활성화하여 stencil testing을 활성화시킬 수 있습니다. 이 시점부터 호출되는 모든 렌더링 명령은 stencil buffer의 영향을 받습니다.


glEnable(GL_STENCIL_TEST);    

  또한 color, depth buffer와 마찬가지로 매 렌더링 루프마다 stencil buffer를 비워주어야 한다는 것을 알아두세요.


glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); 

  또한, depth testing의 glDepthMask 함수처럼, stencil buffer에도 동일한 기능의 함수가 존재합니다. 이 glStencilMask 함수는 곧 buffer에 작성될 stencil 값에 AND 연산을 시킬 bitmask를 설정할 수 있도록 해줍니다. 기본값으로 bitmask는 모두 1로 설정되어 출력에 아무런 영향을 주지않습니다. 하지만 우리가 이것을 0x00으로 설정하면 buffer에 작성되는 모든 stencil 값들은 0이 됩니다. 이는 dpeth testing의 glDepthMask(GL_FALSE) 함수와 비슷합니다.


glStencilMask(0xFF); // 각 비트들은 stencil buffer 그대로 작성됩니다.
glStencilMask(0x00); // 각 비트들은 stencil buffer에 0으로 작성됩니다(작성 비활성화).

  대부분의 경우에 여러분은 stencil mask를 0x000xFF로 설정할 것입니다. 하지만 임의의 bitmask들을 설정할 수 있는 옵션들도 존재한다는 사실을 알아두세요.

Stencil 함수

  Depth testing과 마찬가지로 stencil test를 통과시킬지 말지에 대한 기준을 설정할 수 있습니다. stencil testing을 설정할 수 있는 총 2개의 함수가 있습니다. glStencilFunc 함수와 glStencilOp 함수 입니다.


  이 glStencilFunc(GLenum func, GLint ref, GLuint mask) 함수는 3개의 파라미터를 가지고 있습니다.


  • func: stencil test 함수를 설정합니다. 이 test 함수는 저장된 stencil 값과 glStencilFunc 함수의 ref 값에 적용됩니다. 가능한 옵션은 GL_NEVER, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL, GL_EQUAL, GL_NOTEQUAL, GL_ALWAYS가 있습니다. 이 것들의 의미는 depth buffer의 함수들과 비슷합니다.
  • ref: stencil test에 대한 레퍼런스 값을 지정합니다. stencil buffer의 내용은 이 값과 비교됩니다.
  • mask: 그들을 비교하기 전에 레퍼런스 값과 저장된 stencil 값 모두에 AND 연산이 수행되어질 mask를 지정합니다. 초기값으로는 모두 1로 설정됩니다.

  위의 간단한 stencil 예제의 경우에서는 다음과 같은 함수가 사용되었을 것입니다.


glStencilFunc(GL_EQUAL, 1, 0xFF)

  이 것은 OpenGL에게 fragment의 stencil 값이 레퍼런스 값인 1과 동일(GL_EQUAL)하다면 test를 통과시킨 후 렌더링하고 그렇지 않으면 폐기하라고 지시합니다.

  하지만 glStencilFunc 함수는 오직 OpenGL이 stencil buffer의 내용으로 무엇을 해야하는지에 대해서만 묘사하고 우리가 실제로 buffer를 수정할 수 있는 방법에 대해서는 다루지 않습니다. 이는 glStencilOp 함수에서 다루어집니다.

  glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass) 함수는 3개의 옵션을 가지고 있고 각 옵션들에 대해 취해질 액션들을 설정할 수 있습니다.


  • sfail: stencil test가 실패하였을 때 취할 행동
  • dpfail: stencil test가 통과했지만 depth test는 실패했을 때 취할 행동
  • dppass: stencil, depth test 모두 통과했을 때 취할 행동

  각 옵션들에 대해 다음과 같은 행동들을 설정할 수 있습니다.


행동 설명
GL_KEEP 현재 저장된 stencil 값을 유지
GL_ZERO stencil 값을 0으로 설정
GL_REPLACE stencil 값을 glStencilFunc 함수에서 지정한 레퍼런스 값으로 설정
GL_INCR 최댓값보다 작다면 stencil 값을 1만큼 증가시킴
GL_INCR_WRAP GL_INCR와 같지만 최댓값을 초과하면 0로 돌아옴
GL_DECR 최솟값보다 크다면 stencil 값을 1만큼 감소시킴
GL_DECR_WRAP GL_DECR와 같지만 0보다 작다면 최댓값으로 설정함
GL_INVERT 현재 stencil buffer 값의 비트를 뒤집음

  glStencilOp 함수의 기본값은 (GL_KEEP, GL_KEEP, GL_KEEP)이므로 test의 결과가 어떻든 stencil buffer의 값은 유지됩니다. 기본 행동은 stencil buffer를 수정하지 않는 것이므로 stencil buffer를 수정하고 있다면 옵션 중 하나라도 다른 행동으로 바꾸어야 합니다.


  그래서 glStencilFunc 함수와 glStencilOp 함수를 사용하면 언제 그리고 어떻게 stencil buffer를 수정해야 하는지를 정확히 지정할 수 있고 또한 언제 stencil test가 통과하거나 실패할지도 지정할 수 있습니다.

Object outlining

  이전 섹션에서 stencil testing이 어떻게 작동하는지에 대해 완전히 이해가되지 않으실 것입니다. 그래서 stencil testing으로 구현될 수 있는 유용한 기능인 object outlining를 설정드리겠습니다.


An object outlined using stencil testing/buffer

  Object outlining은 말 그대로 입니다. 각 오브젝트(또는 오직 하나)에 대해 색이 입혀진 작은 외곽선을 생성합니다. 이는 예를 들면 전략 게임에서 유닛들을 선택하고 싶을 때 사용자가 어떠한 유닛들을 선택했는지 보여주어야 할 때 특히 유용한 효과입니다. 여러분의 오브젝트를 outlining하기 위한 과정은 다음과 같습니다.


  1. 오브젝트를 그리기 전에 stencil 함수를 GL_ALWAYS로 설정하고 오브젝트의 fragment가 렌더링될때마다 stencil buffer를 1로 수정합니다.
  2. 오브젝트를 렌더링합니다.
  3. stencil 작성과 depth testing을 비활성화합니다.
  4. 각 오브젝트들을 약간 확대합니다.
  5. 하나의 (외곽선) 컬러를 출력하는 별도의 fragment shader를 사용합니다.
  6. 오브젝트를 다시 그리지만 stencil 값이 1과 같지 않은 fragment들만 그립니다.
  7. 다시 stencil 작성과 depth testing을 활성화합니다.

  이 과정은 각 오브젝트의 fragment들에 대해 stencil buffer의 내용을 1로 설정하고 외곽선을 그리고 싶을 때 오브젝트의 확대된 버전을 stencil test가 통과된 부분만 그립니다. 확대된 버전은 오브젝트의 외곽선으로 그려집니다. 우리는 기본적으로 stencil buffer를 사용하여 확대된 버전과 원본 오브젝트가 겹치는 부분의 fragment는 폐기합니다.


  우리는 먼저 외곽선 컬러를 출력하는 아주 기본적인 fragment shader를 생성할 것입니다. 간단히 컬러 값을 하드코딩하고 이 shader를 shaderSingleColor라고 부르겠습니다.


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

  우리는 오직 두개의 컨테이너에만 외곽선을 추가할 것이기 때문에 바닥은 그대로 둡니다. 따라서 우리는 먼저 바닥을 그린 후 (stencil buffer를 작성하면서) 두개의 컨테이너를 그리고 (전에 그려진 컨테이너 fragment들을 기반으로 fragment들을 폐기하면서) 확대된 컨테이너들을 그립니다.


  먼저 stencil testing을 활성화하고 test가 성공하거나 실패했을 때의 행동을 설정합니다.


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

  어떠한 테스트라도 실패했다면 아무 행동도 하지 않고 현재 저장된 값들을 유지시킵니다. 하지만 stencil test와 depth test 모두 성공했다면 저장된 stencil 값을 glStencilFunc 함수를 통해 지정된 레퍼런스 값(나중에 1로 설정할 것)으로 수정합니다.


  stencil buffer를 0로 비우고 컨테이너들을 위해 각 그려진 fragment들에 대해 stencil buffer를 1로 수정합니다.


glStencilFunc(GL_ALWAYS, 1, 0xFF); // 모든 fragment들은 stencil buffer를 수정해야합니다
glStencilMask(0xFF); // stencil buffer 작성 활성화
normalShader.use();
DrawTwoContainers();

  GL_ALWAYS stencil testing 함수를 사용하여 컨테이너의 각 fragment들이 stencil buffer를 수정하여 stencil 값을 1로 만듭니다. 이 fragment들은 stencil test에 항상 통과되기때문에 stencil buffer는 이들이 그려질때마다 레퍼런스 값으로 수정됩니다.


  이 stencil buffer에 컨테이너가 그려진 자리가 1로 수정되었으면 확대된 컨테이너를 그려야 합니다. 하지만 이번에는 stencil buffer 작성을 비활성화 합니다.


glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // stencil buffer 작성 비활성화
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use(); 
DrawTwoScaledUpContainers();

  stencil 함수를 GL_NOTEQUAL로 설정하여 1과 같지 않는 부분만 그리도록하여 이전에 그렸던 컨테이너의 바깥쪽 부분만 그리게 하였습니다. 또한 depth testing을 비활성화하여 바닥에 의해 가져리지 않게 하였습니다.


  또한 수행하고 나면 depth buffer를 다시 활성화시킵니다.


  전체 object outlining 과정은 다음과 같습니다.


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); // 바닥을 그리는 동안에는 stencil buffer를 수정하지 않습니다
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);
glEnable(GL_DEPTH_TEST);  

  stencil testing에 대한 개념을 이해한다면 이 코드가 이해하기 어렵지는 않을 것입니다. 이해하기 어렵다면 이전 섹션을 다시 주의깊게 읽어보시고 각 함수들이 예제에서 어떻게 사용되어지고 있는지 완전히 이해하려고 노력해보세요.


  이 outlining 알고리즘의 결과는 다음과 같이 보여질 것입니다.


3D scene with object outlining using a stencil buffer

  여기에서 전체 소스 코드를 확인하세요.

  두 컨테이너의 외곽선이 겹쳐지는 것을 볼 수 있을 겁니다. 이는 일반적으로 우리가 원하는 효과입니다(전략 게임에서 10마리의 유닛을 선택하는 것을 생각해보세요: 외곽선이 합쳐지는 것이 일반적입니다). 여러분이 오브젝트의 완벽한 외곽선을 원한다면 오브젝트마다 stencil buffer를 비우고 약간의 새로운 depth buffer를 생성해야 합니다.

  이 object outlining 알고리즘은 여러 게임에서 오브젝트의 선택을 시각화할 때(전략 게임)에 꽤 흔히 사용됩니다. 그리고 이러한 알고리즘은 model 클래스와 함게 쉽게 구현될 수 있습니다. 여러분은 간단히 외곽선을 그릴지 말지를 결정하는 boolean flag를 model 클래스에 생성할 수 있습니다. 좀 더 창의적이게 하고 싶다면 외곽선에 좀 더 자연스러운 효과를 넣기 위해 Gaussian Blur와 같은 전처리 필터의 도움을 사용할 수도 있습니다.


  Stencil testing은 outlining 말고도 아주 많은 목적을 가지고 있습니다. 백미러의 텍스처를 그릴 때 거울의 모양에 맞게 그려야 하거나 실시간 그림자를 렌더링할 때 shadow volumes라고 불리는 stencil buffer 기술이 쓰입니다. stencil buffer는 또다른 아주 멋진 도구입니다.



출처 : https://learnopengl.com, Joey de Vries

반응형