stencil buffer는 특수한 효과를 위한 off-screen buffer로, back buffer 및 depth buffer와 동일한 해상도를 가진다. 따라서, stencil buffer 내의 (i, j)번째 픽셀은 back/depth buffer의 (i, j)번째 픽셀과 대응된다.
이름이 의미하는 것 처럼 stencil buffer는 back buffer의 일정 부분이 렌더링되는 것을 막는 효과를 위해 사용된다. 예를 들어, 거울에 특정 물체를 반사하도록 할려고 한다면, 거울에 반사되는 부분에 대해서만 드로잉을 수행하면 된다. 이 때 거울에 비치지 않는 부분은 렌더링되는 것을 막을 수 있는 있도록 하는 것이 바로 stencil buffer다.
stencil buffer을 이용한 것과 그렇지 않은 것에 대한 그림 보기(m)
|
stencil buffer를 공부하기 위한 가장 좋은 방법은 이전의 활용 예를 확인하는 것이며, 이를 통해 필요에 따른 다양한 응용 능력을 기를 수 있다.
Table of Contents
§ 8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 값 지정
o 8.3.4 더블 블렌딩을 막기 위한 스텐실 버퍼 이용
stencil buffer를 이용하기 위해서는 먼저 Direct3D를 초기화하는 시점에 stencil buffer를 요청해야 하며, 이용할 때 이를 활성화 시켜야 한다. stencil buffer를 활성화/비활성화 하려면 D3DRS_STENCILENABLE 렌더 상태를 IDirect3DDevice9::SetRenderState 통해 true/false로 지정해야 한다. stencil buffer를 디폴트 값으로 되돌리기 위해서는 IDirect3DDevice9::Clear를 이용한다:
// 세번째 인자로 D3DCLEAR_STENCIL을 넣고, 여섯번째 인자는 stencil buffer를 clear하는데 이용될 값을 지정하는 것으로 여기에서는 0을 이용하였다. |
책에서는 다루고 있지 않지만 DirectX 9.0은 그림자 볼륨을 그리는 데 필요한 렌더링 단계를 축소하여 그림자 볼륨의 속도를 향상시켜주는 "Two-Sided Stencil" 기능을 포함하고 있다. 자세한 내용은 여기에서 관련된 토픽을 찾아보자.
"Creating Reflections and Shadows Using Stencil Buffers (Mark J. Kilgard)"에 따르면 depth buffer를 이용하는 요즘의 하드웨어에서는 stencil buffer를 이용하는데 드는 비용이 거의 존재하지 않는다고 한다.
stencil buffer는 depth buffer를 만들 때 함께 만들 수 있으며, depth buffer의 포맷을 지정할 때 stencil buffer의 포맷도 함께 지정할 수 있다. 실제로 stencil buffer와 depth buffer는 동일한 off-screen 버퍼를 공유하며, 각 픽셀 내의 메모리 세그먼트만 해당 buffer로 이용될 뿐이다.
D3DFMT_D24S8 32비트 depth/stencil buffer를 만들어 depth buffer에는 픽셀 당 24비트를, stencil buffer에는 픽셀 당 8비트를 할당한다. D3DFMT_D24X4S4 32비트 depth/stencil buffer를 만들어 depth buffer에는 픽셀 당 24비트를, stencil buffer에는 픽셀 당 4비트를 할당한다(나머지 4비트는 사용하지 않는다). D3DFMT_D15S1 32비트 depth/stencil buffer를 만들어 depth buffer에는 픽셀 당 15비트를, stencil buffer에는 픽셀 당 1비트를 할당한다. |
일부 그래픽 카드는 8비트 stencil buffer를 지원하지 않는다.
앞서 언급했듯이 back buffer의 일부 영역이 렌더링되는 것을 막는 데 stencil buffer를 이용할 수 있는데, 여기서 특정 피셀의 렌더링을 막을 것인지의 결정은 stencil test를 통해 이루어지며, 이는 다음과 같은 표현식으로 나타낼 수 있다.
(참조 & 마스크) 비교 연산자 (값 & 마스크) |
stencil이 활성화되어 있다는 가정하에 모든 픽셀에 대해 stencil test가 수행되며, 이 때 두 개의 피연산자를 이용한다.
|
비교 연산자에 지정된 방법으로 LHS와 RHS를 비교하는 stencil test를 수행한 후에 값이 true이면 back buffer의 픽셀을 출력하며, false이면 픽셀이 출력되지 않는다. 당연하지만, back buffer에 픽셀이 쓰여지지 않으면 depth buffer에도 쓰여지지 않는다.
Direct3D는 stencil test에 이용되는 변수들을 제어(참조/매스크 값과 비교 연산자)할 수 있는 방법을 제공한다.
stecil 참조 값은 디폴트로 0이지만 D3DRS_STENCILREF 렌더 상태를 이용해 값을 바꿀 수 있다.
Device->SetRenderState( D3DRS_STENCILREF, 0x1 ); |
stencil 마스크 값은 참조와 값 변수 양쪽의 비트를 마스크하는 데에 이용된다. 디폴트 마스크는 0xffffffff이며, 이는 어떤 비트도 마스크하지 않겠다는 의미이다. D3DRS_STENCILMASK 렌더 상태를 이용하여 변경할 수 있다.
// 상위 16비트를 마스크한다. |
이 값은 stencil test를 수행하고 있는 현재 픽셀의 stencil buffer 값이다. 각각의 stencil 값을 지정할 수는 없지만 stencil buffer를 clear할 수는 있다. 또한 부가적으로 stencil 렌더 상태를 이용하면 stencil buffer로 쓰여질 것에 대한 제어가 가능하다.
D3DRS_STENCILFUNC 렌더 상태를 이용하여 비교 연산자를 지정할 수 있으며 값으로는 D3DCMPFUNC 열거형의 멤버 중 하나이다:
typedef enum _D3DCMPFUNC { |
특정 픽셀이 back buffer에 쓰여질지 여부를 결정하는 것 이외에도 다음과 같은 세 가지 경우에는 stencil buffer 항목이 갱신되는 방법을 정의할 수 있다:
(i, j)번째 픽셀에서 stencil test가 실패 D3DRS_STENCILFAIL 렌더 상태를 지정하여 이러한 상황이 발생했을 때 stencil buffer 내의 (i, j)번째 항목을 갱신하는 방법을 정의할 수 있다.
(i, j)번째 픽셀에서 depth test가 실패 D3DRS_STENCILZFAIL 렌더 상태를 지정하여 (i, j)번째 항목이 갱신하는 방법을 정의할 수 있다.
(i, j)번째 픽셀에서 depth test와 stencil test가 성공 D3DRS_STENCILPASS 렌더 상태를 지정하여 (i, j)번째 항목을 갱신하는 방법을 정의할 수 있다.
|
여기에서 사용되는 StencilOperation 값에는 다음 상수 중 하나를 사용할 수 있다:
D3DSTENCILOP_KEEP stencil buffer 항목을 변경하지 않는다(현재의 값을 유지한다). D3DSTENCILOP_ZERO stencil buffer 항목을 0으로 지정한다. D3DSTENCILOP_REPLACE stencil buffer 항목을 stencil 참조 값으로 대체한다. D3DSTENCILOP_INCRSAT stencil buffer 항목을 증가시킨다(증가된 값은 최대치를 넘지 않는다). D3DSTENCILOP_DECRSAT stencil buffer 항목을 감소시킨다(감소된 값은 0보다 작지 않다). D3DSTENCILOP_INVERT stencil buffer 항목을 반전시킨다. D3DSTENCILOP_INCR stencil buffer 항목을 증가시킨다(증가된 값이 최대치를 넘을 경우 0으로 돌려진다). D3DSTENCILOP_DECR stencil buffer 항목을 감소시킨다(감소된 값이 0보다 작을 경우 최대치로 돌려진다). |
지금까지 언급한 렌더 상태 이외에도 stencil buffer에 쓰여지는 모든 값을 마스크하는 쓰기 마스크를 설정할 수도 있다. 쓰기 마스크의 디폴트 값은 0xffffffff이며, D3DRS_STENCILWRITEMASK 렌더 상태를 이용하여 지정할 수 있다:
// 상위 16비트를 마스크한다. |
예제 파일 다운로드: chapter8_source.zip (8장 예제 모두 포함)
단순한 구현을 위해서 평평한 표면의 거울을 구현하는 것으로 내용을 제한한다. 거울을 구현하기 위해서는 다음 두 가지 문제를 해결해야 한다:
올바르게 반사를 그려내기 위해 임의의 평면에 물체가 반사되는 방법을 알아야 한다. 벡터 기하학을 통해 해결할 수 있다. 거울 영역에만 반사 효과가 나타나도록 해야 한다. stencil buffer를 통해 해결할 수 있다. |
임의의 평면 n^ . p + d = 0에 점 v = (vx, vy, vz)에 반사된 점 v' = (v'x, v'y, v'z)를 계산하는 방법을 알아본다. 다음 그림을 참고하자:
PART 1의 "평면" 섹션에서 q = v - kn^임을 배웠다. k는 v에서 평면으로의 부호를 가진 최단거리이므로, 평면 (n^, d)에 대한 v의 반사는 다음과 같이 얻을 수 있다:
v' = v - 2kn^ |
v에서 v'로의 변환은 다음 행렬로 표현할 수 있다.
D3DX 라이브러리는 R과 같은 임의의 평면에 대한 반사 행렬을 만들어내는 함수, D3DXMatrixReflect을 제공한다:
D3DXMATRIX *WINAPI D3DXMatrixReflect( |
여기에서는 반사 변환에 대해 살펴보고 있으므로 반사 변환의 세 가지 특수한 경우를 확인해보도록 하자. 세 가지 특수한 경우란 표준 좌표 평면인 yz 평면, xz 평면, xy 평면을 말하는 것으로 다음과 같은 세 가지의 행렬을 통해 나타낼 수 있는데, 만약, yz 평면 반대쪽의 포인트를 반사하기 위해서는 x성분의 반대를 취하면 된다. 나머지 경우도 비슷하다.
거울을 구현할 때 중요한 요점 한 가지는 거울의 앞에서만 반사된다는 것이다. 이 경우 물체가 거울 앞에 있는지를 공간적으로 확인한다면 너무 복잡한 작업이 필요할 것이다. 따라서, 거울을 포함하는 모든 표면에서 항상 물체를 반사시키도록 렌더링하는 방법을 선택하고, stencil buffer를 이용하여 back buffer내의 특정 영역이 렌더링되는 것을 막아준다. 이와 같은 작업을 위해서는 다음과 같은 순서를 따른다:
1. 바닥과 벽, 거울, 주전자를 포함하는 전체 장면을 (보통 때와 마찬가지로) 렌더링한다. 아직 주전자의 반사는 포함되지 않으며, 이 단계에서는 아직 stencil buffer를 수정하지 않는다. 2. stencil buffer를 0으로 clear한다. 3. 거울을 구성하는 기본형을 stencil buffer에만 렌더링한 다음, stencil test가 항상 성공(D3DCMP_ALWAYS)하도록 렌더 상태를 변경하고 test가 성공하면 stencil buffer 항목을 1로 대체(D3DSTENCILOP_REPLACE)하도록 지정한다. 이렇게 하면 거울만을 렌더링하는 것이므로 거울에 해당하는 픽셀만 1이란 값을 가지며, 나머지 영역은 0이 된다. 4. 이제 반사된 주전자를 back buffer와 stencil buffer로 렌더링한다. 하지만 이번에는 stencil test를 통과한 부분만 back buffer에 렌더링 된다. 즉, stencil buffer 항목이 1인 경우에만 테스트를 통과하도록 지정한다. stencil buffer 내의 거울에 해당하는 항목만이 1 값을 가지므로 반사된 주전자는 거울에만 렌더링된다. |
예제와 관련된 코드는 RenderMirror 함수에 포함되어 있다. 이 함수는 stencil buffer에 거울을 렌더링하고, 거울에 해당되는 부분에만 반사된 주전자를 렌더링 한다. 코드를 단계적으로 살펴보자.
8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 값 지정
Device->SetRenderState(D3DRS_STENCILENABLE, true); // 스텐실 활성화 |
D3DRS_ZWRITEENABLE을 false로 지정하면 depth buffer로 쓰여지는 것을 막을 수 있다. 또, D3DRS_SRCBLEND를 D3DBLEND_ZERO로, D3DRS_DESTBLEND를 D3DBLEND_ONE으로 지정하고 블렌딩을 이용하면 back buffer가 갱신되는 것을 막을 수 있다. 이는 blending 방정식에 직접 인수를 넣어 확인할 있다:
Pixelresult |
// disable writes to the depth and back buffers |
이제 stencil buffer내의 거울에 해당하는 픽셀은 0x1 값을 가지게 된다. 이제 주전자 렌더링을 준비한다.
// only draw reflected teapot to the pixels where the mirror was drawn to. |
새로운 비교 연산자를 지정하고 다음과 같이 stencil test를 구성한다:
(ref & mask) == (value & mask) |
이제, 반사된 주전자 중에서 거울에 비취는 부분만 렌더링된다.
반사될 위치를 지정하기 위해 먼저 반사되지 않은 주전자 위치로 이동한 뒤, xy 평면으로 반사를 수행한다(변환의 순서는 행렬을 곱하는 순서에 따라 정해진다).
// position reflection |
// 반사된 주전자의 깊이가 거울의 깊이보다 크므로(거울이 반사된 주전자를 가리므로), |
예제 파일 다운로드: chapter8_source.zip (8장 예제 모두 포함)
이와 같은 형태의 그림자는 매우 간소화된 것으로 장면의 사실감을 높여주기는 하지만 그림자 볼륨과 같이 사실적이지는 못하다. 자세한 내용은 여기에서 관련된 토픽을 찾아보도록 하자.
평면 그림자를 구현하기 위해서는 먼저 물체가 만들어내는 그림자를 찾고 렌더링할 수 있도록 기하학적으로 구성해야 한 다음, 약간의 투명도(예제에서는 50%)를 가진 검은 재질을 이용해 렌더링 하면 된다. 이와 같은 렌더링에는 "double blending"이라 불리는 부작용이 따르는데, stencil buffer를 이용해 이를 해결할 수 있다. 이에 대해서는 뒤에서 살펴본다.
그림에서 L 방향을 가지는 평행 광원에서 점 p로 통하는 광선은 r(t) = p + tL로 얻을 수 있고, 광선 r(t)와 평면 n . p + d = 0을 교차하면 s를 얻을 수 있다. 또한 광선 r(t)를 물체의 각 점에서 평면으로 발사해 얻은 교차점의 집합으로 그림자의 기하 정보를 정의할 수 있다. 교차점 s는 광선/평면 교차 테스트를 통해 간단히 얻을 수 있다:
1. n . (p + tL) + d = 0 : r(t)을 평면 방정식에 넣는다. 2. n . p + t(n . L) = -d 3. t(n . L) = -d - n . p : t를 풀어낸다. 4. t = (-d - n . p) / (n . L) ∴ s = p + ((-d - n . p) / (n . L))L |
그림에서 점 L 위치에 존재하는 점 조명 광원에 의해 발생하는 그림자를 보여주고 있다. 점 조명에서 점 p로 발사되는 광선은 r(t) = p + t(p - L)로 얻을 수 있으며, 광선 r(t)와 평면 n . p + d = 0과의 교차점으로 s를 얻을 수 있다. 광선 r(t)를 물체의 각 점에서 평면으로 발사해 얻은 교차점의 집합으로 그림자의 기하 정보를 얻을 수 있으며, 평행 그림자에서와 같은 방법(평면/광선 교차)으로 s를 풀어낼 수 있다.
평행 조명을 보여주는 그림에서 그림자는 결국 지정된 광선 방향으로 평면 n . p + d = 0에 물체를 평행 투영한 것이다. 비슷하게 점 조명을 보여주는 그림자는 광원의 관점에서 물체를 평면 n . p + d = 0에 원근 투영한 것이다.
점 p에서 평면 n . p + d = 0으로의 투영 s는 하나의 행렬로 표현될 수 있다. 또한, 동일한 행렬로 직각 투영과 원근 투영을 동시에 표현하는 것도 가능하다.
그림자가 만들어질 평면의 공통 평면 방정식은, 계수로 4D 벡터 (nx, ny, nz, d)를 이용하고, 평행 조명의 방향이나 점 조명의 위치를 표현하는 4D 벡터로 L = (Lx, Ly, Lz, Lw)를 이용하자. w는 다음과 같이 이용된다:
1. w = 0이면, L은 평행 조명의 방향을 나타낸다. 2. w = 1이면, L은 점 조명의 위치를 나타낸다. |
평면의 법선이 정규화되었다고 가정하고 k = (nx, ny, nz, d) . (Lx, Ly, Lz, Lw) = nxLx + nyLy + nzLz + dLw라고 하자.
이제 다음의 그림자 행렬을 이용해 점 p에서 투영 s로의 변환을 나타낼 수 있다.
행렬의 유도 과정은 우리에게 그다지 중요한 내용이 아니므로 다루지 않는다. 관심 있는 사람은 Jim Blinn's Corner: A Trip Down the Graphics Pipeline에서 6장 "Me and My (Fake) Shadow" 부분을 참고하기 바란다.
D3DX 라이브러리에서는 그림자 행렬을 만들어내는 D3DXMatrixShadow를 제공한다. 결과로 얻어진 행렬은 w = 0일 경우 평행 조명에서, w=1일 경우 점 조명에서 주어진 평면으로 그림자를 투영한다:
D3DXMATRIX *WINAPI D3DXMatrixShadow( |
물체의 기하 정보를 평면에 납작하게 만들어 그림자를 표현하면 두 개 이상의 펴진 삼각형이 겹치는 현상이 발생할 수 있다. (blending을 이용해) 반투명한 그림자를 렌더링하면 겹쳐진 영역들이 여러 차례 블렌드 되어 더욱 어둡게 나타난다. 이를 해결하기 위한 방법이 바로 stencil buffer이다. 즉, 처음으로 렌더링되는 픽셀만을 받아들이도록 stencil test를 구성하여 하나의 픽셀에 두 번 이상 blending이 적용되는 것을 막을 수 있다.
예제와 관련된 코드는 RenderShadow 함수에 포함되어 있다. 여기에서는 stencil buffer를 이미 0으로 clear했다고 가정하고 있다는 점에 주의하자.
void RenderShadow() { // stencil buffer를 이미 0으로 clear했다고 가정한다. /* * 먼저 stencil 렌더 상태를 지정하고, 비교 함수를 D3DCMP_EQUAL로 지정한 다음, * D3DRS_STENCILREF를 0x0으로 지정하여 stencil buffer 내의 대응되는 항목이 0x0일 경우에만 * back buffer에 그림자를 렌더링하도록 하였다. * stencil buffer는 0(0x0)으로 clear되어 있으므로 처음 픽셀을 쓸 때에는 항상 테스트가 성공하지만 * D3DRS_STENCILPASS를 D3DSTENCILOP_INCR로 지정하였으므로 * 이미 쓰여진 픽셀을 쓰려고 할 때는 테스트가 실패한다. * 즉, 픽셀의 덮어쓰기를 막는 방법으로 double-blending 현상을 제거한 것이다 */ Device->SetRenderState(D3DRS_STENCILENABLE, true); Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL); Device->SetRenderState(D3DRS_STENCILREF, 0x0); Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff); Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff); Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP); Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP); Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCR); \// increment to 1 /* * 다음에는 그림자 변환을 계산하고 장면 내의 적절한 위치로 그림자를 이동한다: */ // position shadow D3DXVECTOR4 lightDirection(0.707f, -0.707f, 0.707f, 0.0f); D3DXPLANE groundPlane(0.0f, -1.0f, 0.0f, 0.0f); D3DXMATRIX S; D3DXMatrixShadow( &S, &lightDirection, &groundPlane); D3DXMATRIX T; D3DXMatrixTranslation( &T, TeapotPosition.x, TeapotPosition.y, TeapotPosition.z); D3DXMATRIX W = T * S; Device->SetTransform(D3DTS_WORLD, &W); /* * 마지막으로 50%의 투명도를 갖는 검은 재질을 지정하고 depth test를 비활성화 한 다음 * 그림자를 렌더링 한다. 렌더링 후에는 다시 렌더 상태를 원래대로 되돌려 준다. * z-쟁탈을 막기 위해 depth buffer를 비활성화 하였는데, 바닥을 먼저 렌더링하고 * depth test를 끈 상태로 그림자를 렌더링하면 의도한대로 바닥 위에 그림자를 그릴 수 있다. */ // alpha blend the shadow Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true); Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); D3DMATERIAL9 mtrl = d3d::InitMtrl(d3d::BLACK, d3d::BLACK, d3d::BLACK, d3d::BLACK, 0.0f); mtrl.Diffuse.a = 0.5f; // 50% transparency. // Disable depth buffer so that z-fighting doesn't occur when we // render the shadow on top of the floor. Device->SetRenderState(D3DRS_ZENABLE, false); Device->SetMaterial(&mtrl); Device->SetTexture(0, 0); Teapot->DrawSubset(0); Device->SetRenderState(D3DRS_ZENABLE, true); Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); Device->SetRenderState(D3DRS_STENCILENABLE, false); |