Home [learn-opengl] Advanced OpenGL: Stencil testing
Post
Cancel

[learn-opengl] Advanced OpenGL: Stencil testing

Stencil testing

  • Fragment shader가 fragment를 처리하고 나면, depth test와 비슷한 stencil test라고 불리는 것이 수행된다.

    • fragment가 폐기될지 안될지 테스트하는 것이다.
    • 이를 수행한 후 depth test로 또한번 폐기되는지 테스트한다.
    • stencil buffer라는 버퍼에 의해 수행된다.
    • 이 버퍼는 렌더링 동안에 수정할 수 있다.
  • stencil buffer는 일반적으로 8 비트의 stencil value를 가지고 있음.

    • 이 값은 pixel/fragment마다 256개의 값으로 나타내어진다.
    • 이 stencil 값을 가지고 있는 특정 fragment를 테스트할 수 있다.

각 windowing library들은 stencil buffer를 세팅해야만 작동한다. GLFW는 이를 자동적으로 해준다. 하지만 다른 라이브러리들은 기본적으로 생성하지 않을 수 있으니, 문서를 확인하자

  • 간단한 예제는 아래와 같다. (pixels not-to-scale):

  • 이 stencil buffer는 먼저 0으로 채워지고나서 속이 비어있는 사각형 모양의 1을 설정한다.

    • 이 scene의 fragment 중에서 stencil 값이 1인 fragment들만 렌더링된다.(다른 것들은 폐기된다.)
  • stencil buffer는 fragment를 렌더링할 때마다 버퍼를 특정 값으로 설정할 수 있도록 허용한다.

    • 같은 프레임에서 이러한 값을 읽어 특정 frag를 폐기하거나 전달할 수 있다.
    • 스텐실 버퍼를 마음대로 사용할 수 있지만, 일반적인 아웃라인은 다음과 같다.
  1. 스텐실 버퍼 쓰기 활성화
  2. 스텐실 버퍼의 내용을 업데이트하여 오브젝트 렌더링.
  3. 스텐실 버퍼에 쓰기 비활성화
  4. 스텐실 버퍼를 기반으로 특정 fragment를 폐기하여 (other)오브젝트 렌더링
  • stencil buffer를 사용함으로써 scene에 그려진 다른 오브젝트의 fragment들을 기반으로하여 특정 fragment를 폐기할 수 있음.

  • GL_STENCIL_TEST를 활성화하여 stencil testing을 활성화시킬 수 있다.

    • 이 시점부터 호출되는 모든 렌더링 호출은 어떤 방식으로든 stencil buffer에 영향을 미친다.
1
glEnable(GL_STENCIL_TEST);
  • 또한 color, depth buffer와 마찬가지로, 매 렌더링 루프마다 stencil buffer를 비워주어야 한다.
1
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
  • glStencilMask
    • glDepthMask 함수처럼 동일한 기능을 수행함.
    • 곧 버퍼에 작성될 stencil 값에 AND 연산을 시킬 bitmask를 설정함
    • 기본적으로 bitmask는 모두 1로 설정됨, 출력에 영향주지 않음
    • 이것을 0x00으로 설정하면 buffer에 작성되는 모든 stencil 값들은 0이 된다.
    • 이는 depth testing의 glDepthMask(GL_FALSE) 함수와 비슷하다.
1
2
glStencilMask(0xFF); // each bit is written to the stencil buffer as is
glStencilMask(0x00); // each bit ends up as 0 in the stencil buffer (disabling writes)
  • 대부분 위처럼 0x00 이나 0xFF로 설정한다.
    • 다른 커스텀 비트마스크 옵션들도 존재하긴 함.

Stencil functions

  • stencil test를 통과시킬지 말지에 대한 기준을 설정한다.

    • 2개의 함수가 있음
    • glStencilFunc
    • glStencilOp
  • 두 함스를 사용하면, 언제 어떻게 버퍼를 수정해야하는지를 정확히 지정할 수 있고, 언제 테스트가 통과하거나 실패할지도 지정할 수 있다.

glStencilFunc

  • 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
    • ref: stencil test에 대한 레퍼런스 값을 지정
      • stencil buffer의 내용은 이 값과 비교됨.
    • mask: 비교하기 전에 참조 값과 저장된 스텐실 값 모두에 AND 연산이 수행되어질 마스크를 지정한다. 초기값은 모두 1.
1
glStencilFunc(GL_EQUAL, 1, 0xFF)
  • 위 코드는, OpenGL에게 fragment의 stencil값이 레퍼런스 값인 1과 동일 하다면, stencil test를 통과시킨 후 그리고, 그렇지 않으면 폐기한다.

  • 하지만 이 함수는 오직 OpenGL이 stencil buffer의 내용물읠 기반으로 fragment들이 폐기될지 패스될지만 기술한다.

    • 실제로 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 함수에서 지정한 ref 값으로 설정
    • GL_INCR: 최댓값보다 작다면, stencil값을 1만큼 증가시킴
    • GL_INCR_WRAP: GL_INCR와 같고, 최댓값을 초과하면 0으로 돌아옴
    • GL_DECR: 최솟값보다 크다면, stencil 값을 1만큼 감소시킴
    • GL_DECR_WRAP: GL_DECR와 같고, 0보다 작으면 최댓값으로 설정
    • GL_INVERT: 현재 stencil buffer 값의 비트를 뒤집음
  • 기본값 == (GL_KEEP, GL_KEEP, GL_KEEP)

    • 결과가 어떻든 버퍼의 값 유지됨
    • 기본 행동 == 버퍼를 수정하지 않는것.
    • 버퍼를 덮어쓰고 싶다면 이 옵션을 변경해야함

Object outlining

  • 스텐실 테스트로 구현될 수 있는 유용한 기능중 하나.

  • 각 오브젝트에 대해 색이 입혀진 작은 외곽선을 생성한다.

    • 유닛을 선택했을 때 해당 유닛이 선택됬는지 보여주는 기능을 구현할 때 유용하다.
  • 과정은 다음과 같다.

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

    • 확대된 버전은 오브젝트의 외곽선으로 그려진다.
    • stencil buffer를 사용하여 겹치는 부분의 fragment는 폐기한다.

예제

  • 먼저 외곽선 컬러 fragment shader를 작성해야한다. (간단히 하드코딩)
1
2
3
4
void main()
{
    FragColor = vec4(0.04, 0.28, 0.26, 1.0);
}
  • depth test의 앞선 예제를 사용하여, 두 컨테이너에 외곽선을 추가할것이다.

    • 먼저 바닥을 그린후
    • stencil을 쓰면서 두 컨테이너를 그린다.
    • 그리고 스케일업된 두 컨테이너를 그린다.(곂치는 부분의 frag를 폐기하면서)
  • 먼저 stencil test를 활성화하자

1
glEnable(GL_STENCIL_TEST);
  • 그다음에 각 프레임마다 수행하기를 원하는 행동을 정의하자.
    • 모든 테스트가 성공할 때, 지정된 ref값으로 수정하게 설정
    • stencil buffer를 0으로 비우고
    • 컨테이너들을 위해 각 그려진 fragment들에 대해 stencil buffer를 1로 업데이트한다.
1
2
3
4
5
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilFunc(GL_ALWAYS, 1, 0xFF); // all fragments should pass the stencil test
glStencilMask(0xFF); // enable writing to the stencil buffer
normalShader.use();
DrawTwoContainers();
  • GL_ALWAYS로 각 fragment들이 버퍼를 수정하여 stencil 값을 1로 만든다.
    • 항상 통과, 그려지는곳에 버퍼가 ref값 즉 1로 수정
  • 그 후, 확대된 컨테이너를 그려야함.
    • 이번에는 버퍼 쓰기를 비활성화한다.
    • 그리고 1과 같지않은 부분만 그리도록한다.(NOTEQUAL) => 외곽선
    • 그리고 depth test를 비활성화하여, 바닥에 의해 덮어쓰여지지 않도록했다.(?)
1
2
3
4
5
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // disable writing to the stencil buffer
glDisable(GL_DEPTH_TEST);
shaderSingleColor.use();
DrawTwoScaledUpContainers();
  • 전체 과정은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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); // make sure we don't update the stencil buffer while drawing the floor
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);
  • outlining 알고리즘의 결과는 다음과 같다.

전체 코드

  • 두 컨테이너의 외곽선이 겹쳐짐. 이처럼 합치는것은 일반적으로 작은 유닛을 선택할 때 유용하다.

    • 완벽한 외곽선을 원한다면, 오브젝트마다 stencil buffer를 비우고, 약간 창의적으로 depth buffer를 사용해야한다.
  • 이런 외곽선은 여러 게임(특히 전략게임)에서 오브젝트를 선택을 시각화할 때 흔히 사용된다.

    • 이러한 알고리즘은 model 클래스와 함께 쉽게 구현 가능하다.
    • 간단히 외곽선을 그릴지 말지를 결정하는 boolean flag를 model 클래스에 생성할 수 있다.
    • 좀 더 창의적이게 자연스러운 효롸를 넣고 싶다면, Gaussian Blur와 같은 전처리 필터를 사용할 수 있다.
  • stencil testing은 다른 목적으로 사용가능하다.

    • 백미러의 텍스처를 그릴 때, 거울의 모양에 맞게 그리거나
    • 실시간 그림자를 렌더링할 때 shadow volumes라는 stencil buffer 기술이 쓰임.

출처

stencil-testing: 원문 stencil-testing: 번역본

This post is licensed under CC BY 4.0 by the author.

[learn-opengl] Advanced OpenGL: Depth testing

[백준][C++] 2206: 벽부수고 이동하기 (Floyd Warshall)