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)

Figure. stencil buffer 이용하지 않고 그림

Figure. stencil buffer 이용하여 그림

stencil buffer 공부하기 위한 가장 좋은 방법은 이전의 활용 예를 확인하는 것이며, 이를 통해 필요에 따른 다양한 응용 능력을 기를 있다.

Table of Contents

· 8.1 스텐실 버퍼 이용하기

o 8.1.1 스텐실 버퍼 요청하기

o 8.1.2 스텐실 테스트

o 8.1.3 스텐실 테스트 제어하기

§ 8.1.3.1 스텐실 참조

§ 8.1.3.2 스텐실 마스크

§ 8.1.3.3 스텐실

§ 8.1.3.4 비교 연산자

o 8.1.4 스텐실 버퍼 갱신하기

o 8.1.5 스텐실 쓰기 마스크

· 8.2 예제 애플리케이션: 거울

o 8.2.1 반사를 위한 수학

o 8.2.2 거울 구현의 개관

o 8.2.3 코드와 설명

§ 8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 지정

§ 8.2.3.2 스텐실 버퍼에 거울 렌더링

§ 8.2.3.3 거울로 렌더링 부분 표시

§ 8.2.3.4 장면 내에서 반사될 위치 지정

§ 8.2.3.5 반사된 주전자 그리기

· 8.3 예제 애플리케이션: 평면 그림자

o 8.3.1 평행 그림자

o 8.3.2 조명 그림자

o 8.3.3 그림자

o 8.3.4 더블 블렌딩을 막기 위한 스텐실 버퍼 이용

o 8.3.5 코드와 설명

8.1 스텐실 버퍼 이용하기

stencil buffer 이용하기 위해서는 먼저 Direct3D 초기화하는 시점에 stencil buffer 요청해야 하며, 이용할 이를 활성화 시켜야 한다. stencil buffer 활성화/비활성화 하려면 D3DRS_STENCILENABLE 렌더 상태를 IDirect3DDevice9::SetRenderState 통해 true/false 지정해야 한다. stencil buffer 디폴트 값으로 되돌리기 위해서는 IDirect3DDevice9::Clear 이용한다:

// 세번째 인자로 D3DCLEAR_STENCIL 넣고, 여섯번째 인자는 stencil buffer clear하는데 이용될 값을 지정하는 것으로 여기에서는 0 이용하였다.
Device->Clear( 0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER | D3DCLEAR_STENCIL, 0xff000000, 1.0f, 0 );
...
Device->SetRenderState( D3DRS_STENCILENABLE, true );
...
// stencil 관련 작업을 수행한다.
...
Device->SetRenderState( D3DRS_STENCILENABLE, false );

책에서는 다루고 있지 않지만 DirectX 9.0 그림자 볼륨을 그리는 필요한 렌더링 단계를 축소하여 그림자 볼륨의 속도를 향상시켜주는 "Two-Sided Stencil" 기능을 포함하고 있다. 자세한 내용은 여기에서 관련된 토픽을 찾아보자.

"Creating Reflections and Shadows Using Stencil Buffers (Mark J. Kilgard)" 따르면 depth buffer 이용하는 요즘의 하드웨어에서는 stencil buffer 이용하는데 드는 비용이 거의 존재하지 않는다고 한다.

8.1.1 스텐실 버퍼 요청하기

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 지원하지 않는다.

8.1.2 스텐실 테스트

앞서 언급했듯이 back buffer 일부 영역이 렌더링되는 것을 막는 stencil buffer 이용할 있는데, 여기서 특정 피셀의 렌더링을 막을 것인지의 결정은 stencil test 통해 이루어지며, 이는 다음과 같은 표현식으로 나타낼 있다.

(참조 & 마스크) 비교 연산자 ( & 마스크)

stencil 활성화되어 있다는 가정하에 모든 픽셀에 대해 stencil test 수행되며, 개의 피연산자를 이용한다.

  • 왼쪽 피연산자(LHS, 참조 & 마스크): 애플리케이션에서 정의한 stencil 참조 값과 마스크 값의 AND 연산으로 얻어진다.
  • 오른쪽 피연산자(RHS, & 마스크): 현재 테스트하려는 픽셀의 stencil buffer 애플리케이션에서 정의한 마스크 값의 AND 연산으로 얻어진다.

비교 연산자에 지정된 방법으로 LHS RHS 비교하는 stencil test 수행한 후에 값이 true이면 back buffer 픽셀을 출력하며, false이면 픽셀이 출력되지 않는다. 당연하지만, back buffer 픽셀이 쓰여지지 않으면 depth buffer에도 쓰여지지 않는다.

8.1.3 스텐실 테스트 제어하기

Direct3D stencil test 이용되는 변수들을 제어(참조/매스크 값과 비교 연산자) 있는 방법을 제공한다.

8.1.3.1 스텐실 참조

stecil 참조 값은 디폴트로 0이지만 D3DRS_STENCILREF 렌더 상태를 이용해 값을 바꿀 있다.

Device->SetRenderState( D3DRS_STENCILREF, 0x1 );
// 16진수를 이용하면 정수의 비트 배열을 확인하거나 AND 등의 비트 연산 시에 유리하다.

8.1.3.2 스텐실 마스크

stencil 마스크 값은 참조와 변수 양쪽의 비트를 마스크하는 데에 이용된다. 디폴트 마스크는 0xffffffff이며, 이는 어떤 비트도 마스크하지 않겠다는 의미이다. D3DRS_STENCILMASK 렌더 상태를 이용하여 변경할 있다.

// 상위 16비트를 마스크한다.
Device->SetRenderState( D3DRS_STENCILMASK, 0x0000ffff );

8.1.3.3 스텐실

값은 stencil test 수행하고 있는 현재 픽셀의 stencil buffer 값이다. 각각의 stencil 값을 지정할 수는 없지만 stencil buffer clear 수는 있다. 또한 부가적으로 stencil 렌더 상태를 이용하면 stencil buffer 쓰여질 것에 대한 제어가 가능하다.

8.1.3.4 비교 연산자

D3DRS_STENCILFUNC 렌더 상태를 이용하여 비교 연산자를 지정할 있으며 값으로는 D3DCMPFUNC 열거형의 멤버 하나이다:

typedef enum _D3DCMPFUNC {
D3DCMP_NEVER = 1, // stencil test 항상 실패한다.
D3DCMP_LESS = 2, // LHS < RHS 경우 stencil test 성공한다.
D3DCMP_EQUAL = 3, // LHS = RHS 경우 stencil test 성공한다.
D3DCMP_LESSEQUAL = 4, // LHS <= RHS 경우 stencil test 성공한다.
D3DCMP_GREATER = 5, // LHS > RHS 경우 stencil test 성공한다.
D3DCMP_NOTEQUAL = 6, // LHS != RHS 경우 stencil test 성공한다.
D3DCMP_GREATEREQUAL = 7, // LHS >= RHS 경우 stencil test 성공한다.
D3DCMP_ALWAYS = 8, // stencil test 항상 성공한다.
D3DCMP_FORCE_DWORD = 0x7fffffff
} D3DCMPFUNC;

8.1.4 스텐실 버퍼 갱신하기

특정 픽셀이 back buffer 쓰여질지 여부를 결정하는 이외에도 다음과 같은 가지 경우에는 stencil buffer 항목이 갱신되는 방법을 정의할 있다:

(i, j)번째 픽셀에서 stencil test 실패

D3DRS_STENCILFAIL 렌더 상태를 지정하여 이러한 상황이 발생했을 stencil buffer 내의 (i, j)번째 항목을 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILFAIL, StencilOperation );

(i, j)번째 픽셀에서 depth test 실패

D3DRS_STENCILZFAIL 렌더 상태를 지정하여 (i, j)번째 항목이 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILZFAIL, StencilOperation );

(i, j)번째 픽셀에서 depth test stencil test 성공

D3DRS_STENCILPASS 렌더 상태를 지정하여 (i, j)번째 항목을 갱신하는 방법을 정의할 있다.

Device->SetRenderState( D3DRS_STENCILPASS, StencilOperation );

여기에서 사용되는 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보다 작을 경우 최대치로 돌려진다).

8.1.5 스텐실 쓰기 마스크

지금까지 언급한 렌더 상태 이외에도 stencil buffer 쓰여지는 모든 값을 마스크하는 쓰기 마스크를 설정할 수도 있다. 쓰기 마스크의 디폴트 값은 0xffffffff이며, D3DRS_STENCILWRITEMASK 렌더 상태를 이용하여 지정할 있다:

// 상위 16비트를 마스크한다.
Device->SetRenderState( D3DRS_STENCILWRITEMASK, 0x0000ffff );

8.2 예제 애플리케이션: 거울

예제 파일 다운로드: chapter8_source.zip (8 예제 모두 포함)

Figure. 거울 예제 실행 화면

단순한 구현을 위해서 평평한 표면의 거울을 구현하는 것으로 내용을 제한한다. 거울을 구현하기 위해서는 다음 가지 문제를 해결해야 한다:

올바르게 반사를 그려내기 위해 임의의 평면에 물체가 반사되는 방법을 알아야 한다.

벡터 기하학을 통해 해결할 있다.

거울 영역에만 반사 효과가 나타나도록 해야 한다.

stencil buffer 통해 해결할 있다.

8.2.1 반사를 위한 수학

임의의 평면 n^ . p + d = 0 v = (vx, vy, vz) 반사된 v' = (v'x, v'y, v'z) 계산하는 방법을 알아본다. 다음 그림을 참고하자:

Figure. 임의의 평면에 대한 반사. k v에서 평면으로 부호를 가진 최단거리이며, 그림에서 v 평면의 양의 영역에 있으므로 k역시 양수가 된다.

PART 1 "평면" 섹션에서 q = v - kn^임을 배웠다. k v에서 평면으로의 부호를 가진 최단거리이므로, 평면 (n^, d) 대한 v 반사는 다음과 같이 얻을 있다:

v' = v - 2kn^
= v - 2(n^ . v + d)n^
= v - 2( (n^ . v)n^ + dn^ )

v에서 v'로의 변환은 다음 행렬로 표현할 있다.

D3DX 라이브러리는 R 같은 임의의 평면에 대한 반사 행렬을 만들어내는 함수, D3DXMatrixReflect 제공한다:

D3DXMATRIX *WINAPI D3DXMatrixReflect(
D3DXMATRIX *pOut, // 결과 행렬
CONST D3DXPLANE *pPlane // 반사할 평면
);

여기에서는 반사 변환에 대해 살펴보고 있으므로 반사 변환의 가지 특수한 경우를 확인해보도록 하자. 가지 특수한 경우란 표준 좌표 평면인 yz 평면, xz 평면, xy 평면을 말하는 것으로 다음과 같은 가지의 행렬을 통해 나타낼 있는데, 만약, yz 평면 반대쪽의 포인트를 반사하기 위해서는 x성분의 반대를 취하면 된다. 나머지 경우도 비슷하다.

8.2.2 거울 구현의 개관

거울을 구현할 중요한 요점 가지는 거울의 앞에서만 반사된다는 것이다. 경우 물체가 거울 앞에 있는지를 공간적으로 확인한다면 너무 복잡한 작업이 필요할 것이다. 따라서, 거울을 포함하는 모든 표면에서 항상 물체를 반사시키도록 렌더링하는 방법을 선택하고, 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 값을 가지므로 반사된 주전자는 거울에만 렌더링된다.

8.2.3 코드와 설명

예제와 관련된 코드는 RenderMirror 함수에 포함되어 있다. 함수는 stencil buffer 거울을 렌더링하고, 거울에 해당되는 부분에만 반사된 주전자를 렌더링 한다. 코드를 단계적으로 살펴보자.

8.2.3.1 스텐실 버퍼 활성화와 관련 렌더 상태 지정

Device->SetRenderState(D3DRS_STENCILENABLE, true); // 스텐실 활성화
Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); // 스텐실 테스트가 항상 성공하도록 지정
Device->SetRenderState(D3DRS_STENCILREF, 0x1); //
Device->SetRenderState(D3DRS_STENCILMASK, 0xffffffff); //
Device->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff); //
Device->SetRenderState(D3DRS_STENCILZFAIL, D3DSTENCILOP_KEEP); // 깊이 테스트가 실패하면 스텐실 버퍼 항목을 유지함
Device->SetRenderState(D3DRS_STENCILFAIL, D3DSTENCILOP_KEEP); // 스텐실 테스트가 실패하면 스텐실 버퍼 항목을 유지함(D3DCMP_ALWAYS 인해 테스트가 실패하지는 않는다)
Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_REPLACE); // 깊이/스텐실 테스트가 모두 성공하면 스텐실 버퍼 항목을 스텐실 참조 , 0x1 대체

8.2.3.2 스텐실 버퍼에 거울 렌더링

D3DRS_ZWRITEENABLE false 지정하면 depth buffer 쓰여지는 것을 막을 있다. , D3DRS_SRCBLEND D3DBLEND_ZERO, D3DRS_DESTBLEND D3DBLEND_ONE으로 지정하고 블렌딩을 이용하면 back buffer 갱신되는 것을 막을 있다. 이는 blending 방정식에 직접 인수를 넣어 확인할 있다:

Pixelresult
= Pixelsource
(0, 0, 0, 0) + Pixeldestination (1, 1, 1, 1)
= (0, 0, 0, 0) + Pixeldestination
= Pixeldestination

// disable writes to the depth and back buffers
Device->SetRenderState(D3DRS_ZWRITEENABLE, false);
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true);
Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO);
Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

// draw the mirror to the stencil buffer
Device->SetStreamSource(0, VB, 0, sizeof(Vertex));
Device->SetFVF(Vertex::FVF);
Device->SetMaterial(&MirrorMtrl);
Device->SetTexture(0, MirrorTex);
D3DXMATRIX I;
D3DXMatrixIdentity(&I);
Device->SetTransform(D3DTS_WORLD, &I);
Device->DrawPrimitive(D3DPT_TRIANGLELIST, 18, 2);

// re-enable depth writes
Device->SetRenderState( D3DRS_ZWRITEENABLE, true );

8.2.3.3 거울로 렌더링 부분 표시

이제 stencil buffer내의 거울에 해당하는 픽셀은 0x1 값을 가지게 된다. 이제 주전자 렌더링을 준비한다.

// only draw reflected teapot to the pixels where the mirror was drawn to.
Device->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL);
Device->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_KEEP);

새로운 비교 연산자를 지정하고 다음과 같이 stencil test 구성한다:

(ref & mask) == (value & mask)
(0x1 & 0xffffffff) == (value & 0xffffffff)
(0x1) == (value & 0xffffffff)

이제, 반사된 주전자 중에서 거울에 비취는 부분만 렌더링된다.

8.2.3.4 장면 내에서 반사될 위치 지정

반사될 위치를 지정하기 위해 먼저 반사되지 않은 주전자 위치로 이동한 , xy 평면으로 반사를 수행한다(변환의 순서는 행렬을 곱하는 순서에 따라 정해진다).

// position reflection
D3DXMATRIX W, T, R;
D3DXPLANE plane(0.0f, 0.0f, 1.0f, 0.0f);
D3DXMatrixReflect(&R, &plane);

D3DXMatrixTranslation(&T,
TeapotPosition.x,
TeapotPosition.y,
TeapotPosition.z);

W = T * R;

8.2.3.5 반사된 주전자 그리기

// 반사된 주전자의 깊이가 거울의 깊이보다 크므로(거울이 반사된 주전자를 가리므로),
// 현재 상태에서는 반사된 주전자를 그려도 나타나지 않는다. 따라서, depth buffer clear한다.
Device->Clear(0, 0, D3DCLEAR_ZBUFFER, 0, 1.0f, 0);
// 단순히 depth buffer clear하면 반사된 주전자가 거울 전면에 그려지게 되는데,
// 이는 원하는 결과와는 차이가 있다. 따라서,
// depth buffer clear함과 동시에 반사된 주전자를 거울과 섞어(blend) 주어야 한다.

// 거울과 반사된 주전자와의 blending한다:
// result_pixel = source_pixel dest_pixel + dest_pixel (0, 0, 0, 0)
// = source_pixel dest_pixel
Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_DESTCOLOR);
Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ZERO);

// 반사된 주전자를 그릴 준비가 완료 되었다. 이제 반사된 주전자를 그린다.

Device->SetTransform(D3DTS_WORLD, &W); // W 반사된 주전자를 장면 내의 적절한 위치로 이동시킨다.
Device->SetMaterial(&TeapotMtrl);
Device->SetTexture(0, 0);
Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); // 물체가 반사될 때에 물체의 전면과 후면이 뒤바뀌기는데 반해 winding-order 유지된다.
// 따라서, winding-order 변경시키기 위해 후면 추려내기 방법을 변경해야 한다.
Teapot->DrawSubset(0);

// 작업이 끝나면 렌더 상태를 되돌려 준다.
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); // blending 비활성화
Device->SetRenderState( D3DRS_STENCILENABLE, false); // stencil 비활성화
Device->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); // winding-order 원래대로

8.3 예제 애플리케이션: 평면 그림자

예제 파일 다운로드: chapter8_source.zip (8 예제 모두 포함)

Figure. 그림자 예제 실행 화면

이와 같은 형태의 그림자는 매우 간소화된 것으로 장면의 사실감을 높여주기는 하지만 그림자 볼륨과 같이 사실적이지는 못하다. 자세한 내용은 여기에서 관련된 토픽을 찾아보도록 하자.

평면 그림자를 구현하기 위해서는 먼저 물체가 만들어내는 그림자를 찾고 렌더링할 있도록 기하학적으로 구성해야 다음, 약간의 투명도(예제에서는 50%) 가진 검은 재질을 이용해 렌더링 하면 된다. 이와 같은 렌더링에는 "double blending"이라 불리는 부작용이 따르는데, stencil buffer 이용해 이를 해결할 있다. 이에 대해서는 뒤에서 살펴본다.

8.3.1 평행 그림자

Figure. 평행 광원에 의해 발생하는 그림자

그림에서 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

8.3.2 조명 그림자

Figure. 조명 광원에 의해 발생하는 그림자

그림에서 L 위치에 존재하는 조명 광원에 의해 발생하는 그림자를 보여주고 있다. 조명에서 p 발사되는 광선은 r(t) = p + t(p - L) 얻을 있으며, 광선 r(t) 평면 n . p + d = 0과의 교차점으로 s 얻을 있다. 광선 r(t) 물체의 점에서 평면으로 발사해 얻은 교차점의 집합으로 그림자의 기하 정보를 얻을 있으며, 평행 그림자에서와 같은 방법(평면/광선 교차)으로 s 풀어낼 있다.

8.3.3 그림자 행렬

평행 조명을 보여주는 그림에서 그림자는 결국 지정된 광선 방향으로 평면 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(
D3DXMATRIX *pOut, // 결과 행렬
CONST D3DXVECTOR4 *pLight, // L
CONST D3DXPLANE *pPlane // 그림자를 만들 평면
);

8.3.4 더블 블렌딩을 막기 위한 스텐실 버퍼 이용

물체의 기하 정보를 평면에 납작하게 만들어 그림자를 표현하면 이상의 펴진 삼각형이 겹치는 현상이 발생할 있다. (blending 이용해) 반투명한 그림자를 렌더링하면 겹쳐진 영역들이 여러 차례 블렌드 되어 더욱 어둡게 나타난다. 이를 해결하기 위한 방법이 바로 stencil buffer이다. , 처음으로 렌더링되는 픽셀만을 받아들이도록 stencil test 구성하여 하나의 픽셀에 이상 blending 적용되는 것을 막을 있다.

8.3.5 코드와 설명

예제와 관련된 코드는 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);

+ Recent posts