최상단 광고

2012년 4월 9일 월요일

스텐실 버퍼 이용하기



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);



댓글 없음: