개발하는 리프터 꽃게맨입니다.
[물방울책 정리] Chapter 10: Stenciling 본문
스텐실 버퍼는 몇 가지 특수 효과를 구현하기 위해 사용할 수 있는 off-screen 버퍼이다.
스텐실 버퍼는 백 버퍼와 깊이 버퍼와 동일한 해상도를 가지며, 스텔실 버퍼의 ij 번째 픽셀은 백 버퍼와 깊이 버퍼의 ij번째 픽셀과 대응된다.
ID3D11DepthStencilView.. 등의 이름에서 보았다시피 스텐실 버퍼와 깊이 버퍼와 함께 동작한다.
스텐실 버퍼는 스텐실처럼 작동하여 특정 픽셀 조각이 백버퍼로 렌더링되는 것을 차단한다.
예를 들어, 거울을 구현할 떄, 거울의 평면을 기준으로 객체를 반사해야 하지만, 반사된 이미지는 거울 안에만 그려져야 한다. 스텐실 버퍼를 사용하여 반사가 거울에 그려지는 경우가 아니면 렌더링을 차단할 수 있다.
스텐실 버퍼는 ID3D11DepthStencilState 인터페이스를 통해 제어된다.
이 인터페이스는 블렌딩처럼 유연하고 강력한 기능을 제공한다.
스텐실 버퍼를 효과적으로 사용하는 방법은 기존 예제 애플리케이션을 연구함으로써 가장 잘 배울 수 있다.
스텐실 버퍼의 몇 가지 응용 프로그램을 이해하면, 자신의 특정 요구에 맞게 어떻게 사용할지에 대한 더 나은 아이디어를 얻을 수 있을 것이다.
학습 목표:
1. ID3D11DepthStencilState 인터페이스를 사용하여 깊이 및 스텐실 버퍼 설정을 제어하는 방법을 알아본다.
위 그림 왼쪽 그림을 보면, 해골의 일부는 깊이 테스트에 실패하므로 벽돌을 통해 보이지 않는다. 그러나 벽 뒤를 보면 반사가 보이므로, 몰입감이 깨진다.
반사는 거울을 통해서만 나타나야 한다.
2. 스텐실 버퍼를 사용하여 거울이 아닌 표면에 반사가 그려지지 않도록 거울을 구현하는 방법을 배운다.
3. 더블 블렌딩을 식별하고 스텐실 버퍼가 이를 어떻게 방지할 수 있는지 이해한다.
4. 깊이 복잡도를 설명하고, 장면의 깊이 복잡도를 측정할 수 있는 두 가지 방법을 알아본다.
깊이 스텐실 포맷과 초기화
깊이/스텐실 버퍼가 텍스처라는 점을 상기하면, 특정 데이터 포맷으로 생성되어야 한다.
일반적으로 사용된느 포맷은 다음과 같다.
1. DXGI_FORMAT_D32_FLOAT_S8X24_UINT
32비트 부동소수점 깊이 버퍼와 8비트 스텐실 버퍼를 포함하며
스텐실 버퍼는 [0, 255] 범위로 매핑되고, 나머지 24비트는 패딩으로 채워진다.
2. DXGI_FORMAT_D24_UNORM_S8_UINT
24비트 깊이 버퍼가 [0, 1] 범위로 매핑되며, 8비트 스텐실 버퍼는 [0, 255] 범위로 매핑된다.
DXGI_FORMAT_D24_UNORM_S8_UINT 이 포맷을 사용하면 메모리를 덜 사용할 수 있다.
또한, 스텐실 버퍼는 각 프레임의 시작 시에 특정 값으로 초기화되어야 한다.
다음 메소드를 사용하여 이를 수행하며, 이 메소드는 깊이 버퍼도 초기화한다.
void ClearDepthStencilView(
[in] ID3D11DepthStencilView *pDepthStencilView,
[in] UINT ClearFlags,
[in] FLOAT Depth,
[in] UINT8 Stencil
);
ClearFlgas에는 D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL 을 지정해야 둘 다 초기화된다.
Depth는 깊이 버퍼의 각 픽셀에 설정할 flaot 값이며 0에 가까울수록 얕은 깊이, 1에 가까울수록 깊은 깊이를 의미한다.
Stencil은 스텐실 버퍼의 각 픽셀에 설정할 정수값으로 [0, 255] 의 범위를 가진다.
사실 이 함수는 계속 호출하고 있었다.
스텐실 테스트
앞서 언급했듯이, 우리는 스텐실 버퍼를 사용하여 백 버퍼의 특정 영역에 렌더링을 차단할 수 잇다.
특정 픽셀을 쓰지 않기로 결정하는 것은 스텐실 테스트에 의해 결정되며, 이는 다음과 같다.
스텐실 테스트는 출력 병합 단계에서 수행되며, 스텐실링이 활성화된 상태에서 두 개의 피연산자를 사용한다.
1. 왼쪽 피연산자 LHS
애플리케이션에서 정의한 스텐실 참조 값(StencilRef)과 애플리케이션에서 정의한 마스킹 값(StencilReadMask)을 AND 연산하여 결정된다.
2. 오른쪽 피연산자 RHS
테스트할 특정 픽셀의 스텐실 버퍼에 이미 저장된 값과 애플리케이션에서 정의한 마스킹 값을 AND 연산하여 결정된다.
lhs와 rhs 모두 같은 StencilReadMask가 사용된다는 점을 유의해야 한다.
스텐실 테스트는 애플리케이션이 선택한 비교 함수( ⊴ )에 따라 LHS와 RHS를 비교하며, 그 결과는 true 또는 false이다.
테스트 결과가 true이면 (깊이 테스트도 통과한다고 가정할 때) 픽셀을 백 버퍼에 쓴다.
테스트 결과가 false이면, 그 픽셀은 백 버퍼에 쓰이지 않는다.
물론, 스텐실 테스트에 실패하여 픽셀이 거부되면 깊이 버퍼에도 쓰이지 않는다.
⊴ 연산자는 D3D11_COMPARISON_FUNC 열거형에 정의된 함수 중 하나를 사용한다.
D3D11_COMPARISON_NEVER 값: 1 비교를 통과하지 마세요. 즉 항상 false |
D3D11_COMPARISON_LESS 값: 2 원본 데이터가 대상 데이터보다 작으면 비교가 통과됩니다. < |
D3D11_COMPARISON_EQUAL 값: 3 원본 데이터가 대상 데이터와 같으면 비교가 통과합니다. == |
D3D11_COMPARISON_LESS_EQUAL 값: 4 원본 데이터가 대상 데이터보다 작거나 같으면 비교가 통과합니다. <= |
D3D11_COMPARISON_GREATER 값: 5 원본 데이터가 대상 데이터보다 크면 비교가 통과합니다. > |
D3D11_COMPARISON_NOT_EQUAL 값: 6 원본 데이터가 대상 데이터와 같지 않으면 비교가 통과합니다. != |
D3D11_COMPARISON_GREATER_EQUAL 값: 7 원본 데이터가 대상 데이터보다 크거나 같으면 비교가 통과합니다. >= |
D3D11_COMPARISON_ALWAYS 값: 8 항상 비교를 전달합니다. 항상 True |
깊이/스텐실 상태 블록
ID3D11DepthStencilState 인터페이스를 생성하는 첫 번째 단계는 D3D11_DEPTH_STENCIL_DESC 인스턴스를 작성하는 것이다.
typedef struct D3D11_DEPTH_STENCIL_DESC {
BOOL DepthEnable; // 기본값: True
D3D11_DEPTH_WRITE_MASK DepthWriteMask; // 기본값: D3D11_DEPTH_WRITE_MASK_ALL
D3D11_COMPARISON_FUNC DepthFunc; // 기본값: D3D11_COMPARISON_LESS
BOOL StencilEnable; // 기본값: False
UINT8 StencilReadMask; // 기본값: 0xff
UINT8 StencilWriteMask; // 기본값: 0xff
D3D11_DEPTH_STENCILOP_DESC FrontFace;
D3D11_DEPTH_STENCILOP_DESC BackFace;
} D3D11_DEPTH_STENCIL_DESC;
DepthEnable
깊이 버퍼링을 활성화하려면 true로 지정하고, 비활성화하려면 false로 지정한다.
깊이 테스트가 비활성화되면, 드로우 순서가 중요해진다.
이 값이 false면 DepthWriteMask 설정과 관계없이 깊이 버퍼의 요소도 업데이트되지 않는다.
DepthWriteMask
D3D11_DEPTH_WRITE_MASK_ZERO 또는 D3D11_DEPTH_WRITE_MASK_ALL 중 하나만 지정할 수 있다.
DepthEnable이 true로 설정된 경우, D3D11_DEPTH_WRITE_MASK_ZERO는 깊이 버퍼에 쓰기를 비활성화하지만, 깊이 테스트는 계속 발생한다.
D3D11_DEPTH_WRITE_MASK_ALL 는 깊이 버퍼에 쓰기를 활성화한다.
깊이 테스트와 스텐실 테스트가 모두 통과하면 새로운 깊이값이 기록된다.
DepthFunc
일반적으로 D3D11_COMPARISON_LESS를 설정하며, 깊이 테스트가 수행되도록 한다.
이는, 그려질 픽셀의 깊이값이 이미 존재하는 픽셀의 깊이보다 작을 경우 테스트를 통과할 수 있게 해준다.
StencilEnable
스텐실 테스트를 활성화하려면 true로 지정하고, 비활성화하려면 false로 지정한다.
StencilReadMask
스텐실에 사용되는 readmask이다.
StencilWriteMask
스텐실 버퍼가 업데이트될 때, 특정 비트를 마스킹하여 쓰기를 방지할 수 있다.
예를 들어, 상위 4비트를 쓰지 않게 하려면 0x0f 쓰기 마스크를 사용할 수 있다.
기본적으로는 어떤 비트도 마스킹하지 않는다.
FrontFace
앞면 삼각형에 대해 스텐실 버퍼가 어떻게 작동하는지를 나타낸다.
BackFace
뒷면 삼각형에 대해 스텐실 버퍼가 어떻게 작동하는지를 나타낸다.
맨 마지막 2개를 채우려면 D3D11_DEPTH_STENCILOP_DESC를 구성해야 한다.
typedef struct D3D11_DEPTH_STENCILOP_DESC {
D3D11_STENCIL_OP StencilFailOp; // 기본값: D3D11_STENCIL_OP_KEEP
D3D11_STENCIL_OP StencilDepthFailOp; // 기본값: D3D11_STENCIL_OP_KEEP
D3D11_STENCIL_OP StencilPassOp; // 기본값: D3D11_STENCIL_OP_KEEP
D3D11_COMPARISON_FUNC StencilFunc; // 기본값: D3D11_COMPARISON_ALWAYS
} D3D11_DEPTH_STENCILOP_DESC;
StencilFailOp:
스텐실 테스트가 픽셀 프래그먼트에 대해 실패했을 때 스텐실 버퍼를 어떻게 업데이트 할지 설명하는 D3D11_STENCIL_OP
StencilDepthFailOp:
스텐실 테스트는 통과했지만 깊이 테스트는 실패했을 때, 스텐실 버퍼를 어떻게 업데이트할지 설명하는 D3D11_STENCIL_OP
StencilPassOp:
스텐실 테스트와 깊이 테스트가 모두 통과했을 때, 스텐실 버퍼를 어떻게 업데이트할지 설명하는 D3D11_STENCIL_OP
StencilFunc:
스텐실 테스트 비교 함수를 정의하는 D3D11_COMPARISON_FUNC
마지막으로 D3D11_STENCIL_OP 를 살펴보자.
typedef enum D3D11_STENCIL_OP {
D3D11_STENCIL_OP_KEEP = 1,
D3D11_STENCIL_OP_ZERO = 2,
D3D11_STENCIL_OP_REPLACE = 3,
D3D11_STENCIL_OP_INCR_SAT = 4,
D3D11_STENCIL_OP_DECR_SAT = 5,
D3D11_STENCIL_OP_INVERT = 6,
D3D11_STENCIL_OP_INCR = 7,
D3D11_STENCIL_OP_DECR = 8
} D3D11_STENCIL_OP;
D3D11_STENCIL_OP_KEEP:
스텐실 버퍼를 변경하지 않고 현재 값을 유지한다.
D3D11_STENCIL_OP_ZERO :
스텐실 버퍼 항목을 0으로 설정한다.
D3D11_STENCIL_OP_REPLACE :
스텐실 버퍼 항목을 스텐실 테스트에 사용된 스텐실 참조 값(StencilRef)으로 교체한다.StencilRef 값은 깊이 / 스텐실 상태 블록이 렌더링 파이프라인에 바인딩될 때 설정된다.
D3D11_STENCIL_OP_INCR_SAT :
스텐실 버퍼 항목을 증가시킨다.증가된 값이 최대값(예 : 8비트 스텐실 버퍼에서는 255)을 초과하면, 그 항목을 최대값으로 고정(clamp)한다.
D3D11_STENCIL_OP_DECR_SAT :
스텐실 버퍼 항목을 감소시킨다.감소된 값이 0보다 작으면, 그 항목을 0으로 고정(clamp)한다.
D3D11_STENCIL_OP_INVERT :
스텐실 버퍼 항목의 비트를 반전시킨다.
D3D11_STENCIL_OP_INCR :
스텐실 버퍼 항목을 증가시킨다.증가된 값이 최대값을 초과하면 0으로 감싼다(wrap).
D3D11_STENCIL_OP_DECR :
스텐실 버퍼 항목을 감소시킨다.감소된 값이 0보다 작으면 최대값으로 감싼다(wrap).
앞면 삼각형과 뒷면 삼각형에 대한 스텐실 동작은 다를 수 잇다.
백페이스 컬링으로 인해 뒷면 폴리곤을 렌더링하지 않으면 BackFace 설정은 관련이 없지만, 특정 그래픽 알고리즘이나 투명한 지오메트리의 경우 뒷면 폴리곤을 렌더링할 때 BackFace 설정이 중요하다.
깊이 스텐실 상태 생성 및 바인딩
이런 방식으로 생성
바인딩 하는 모습
다른 상태 그룹과 마찬가지로, 기본 깊이/스텐실 상태가 존재하며, 이는 스텐실링이 비활성화된 일반적인 깊이 테스트이다. OMSetDepthStencilState의 첫 번째 매개변수로 null을 전달하여 기본 깊이/스텐실 상태를 복원할 수 있다.
평면 거울 구현
자연에서 많은 표면이 거울 역할을 하여 물체의 반사를 보여준다.
이 섹션에서는 3D 애플리케이션에서 거울을 어떻게 시뮬레이션할 수 있는지 설명한다.
단순화를 위해, 거울 구현 작업을 평면 표현으로만 한정한다.
예를 들어, 반짝이는 자동차는 반사를 보여줄 수 있지만, 자동차의 몸체는 매끄럽고 둥글며 평면이 아니다.
대신, 우리는 반짝이는 대리석 바닥이나 벽에 걸린 거울에 표시되는 반사와 같은 평면에 놓인 거울의 반사를 렌더링한다.
프로그래밍 방식으로 거울을 구현하려면 두 가지 문제를 해결해야 한다.
첫 번째로, 임의의 평면에 대해 물체를 어떻게 반사할 것인지 배워야 하며, 이를 통해 반사를 올바르게 그릴 수 있다.
두 번째로, 반사는 거울에만 표시되어야 하며, 이를 위해 표면을 '거울'로 표시하고, 렌더링할 떄 반사된 물체가 거울 안에 있을 때만 그려져야 한다.
첫 번째 문제는 해석 기하학을 통해 쉽게 해결되며, 두 번째 문제는 스텐실 버퍼를 사용하여 해결할 수 있다.
반사를 그릴 때, 광원도 거울 공간에 존재하는 물체를 기준으로 반사해야 한다.
그렇지 않으면 반사된 조명이 정확하지 않게 된다.
위 그림은 물체의 반사를 그리기 위해 단순히 거울 평면을 기준으로 물체를 반전시켜서 배치하면 된다는 것을 보여준다.
그러나 이것은 이와같은 문제를 야기한다.
즉, 물체의 반사는 장면의 또 다른 객체일 뿐이며, 이를 가리는 것이 없으면 눈에 보이게 도니다.
하지만 반사는 거울을 통해서만 보여야 한다.
스텐실 버퍼를 사용하여 이 문제를 해결할 수 있는데, 스텐실 버퍼는 백 버퍼의 특정 영역에 렌더링을 차단할 수 있기 때문이다.
따라서 거울에 반사되지 않는 해골의 렌더링을 스텐실 버퍼를 사용해 차단할 수 있다.
다음은 이를 구현하는 단계에 대한 개요이다.
1. 바닥, 벽, 해골을 백 버퍼에 정상적으로 렌더링(거울은 제외)한다. 이 단계는 스텐실 버퍼를 수정하지 않는다.
2. 스텐실 버퍼를 0으로 클리어한다. 위 그림은 이 시점은 백 버퍼와 스텐실 버퍼를 보여준다.
3. 거울을 스텐실 버퍼에만 렌더링한다.
백 버퍼로의 색상 쓰기를 비활성화하려면, 블렌드 상태를 생성하여
D3D11_RENDER_TARGET_BLEND_DESC::RenderTargetWriteMask = 0
으로 설정한다.
깊이 버퍼로의 쓰기를 비활성화하려면
D3D11_DEPTH_STENCIL_DESC::DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO3
를 설정한다.
거울을 스텐실 버퍼에 렌더링할 때, 스텐실 테스트가 항상 성공하도록 (D3D11_COMPARISON_ALWAYS) 설정하고, 테스트가 통과되면 스텐실 버퍼의 항목을 1 (StencilRef)로 교체하도록 (D3D11_STENCIL_OP_REPLACE) 설정한다.
깊이 테스트가 실패하면, D3D11_STENCIL_OP_KEEP로 설정하여 깊이 테스트가 실패한 경우 스텐실 버퍼가 변경되지 않도록 한다(예: 해골이 거울의 일부를 가리는 경우).
우리는 거울만 스텐실 버퍼에 렌더링하고 있으므로, 스텐실 버퍼의 모든 픽셀은 거울의 보이는 부분에 해당하는 픽셀을 제외하고 0으로 남는다. 위 그림은 업데이트된 스텐실 버퍼를 보여준다. 본질적으로, 우리는 스텐실 버퍼에서 거울의 보이는 픽셀을 표시하는 것이다.
4. 이제 반사된 해골을 백 버퍼와 스텐실 버퍼에 렌더링한다. 하지만 스텐실 테스트를 통과해야한 백 버퍼에 렌더링된다는 점을 기억하라.
이번에는 스텐실 버퍼의 값이 1일 때만 스텐실 테스트가 성공하도록 설정한다. 이를 위해 스텐실 참조 값(StencilRef)를 1로 설정하고 ,스텐실 연산자로 D3D11_COMPARISON_EQUAL 을 사용한다.
이렇게 하면, 반사된 해골은 스텐실 버퍼에서 값이 1인 영역에만 렌더링된다.
스텐실 버퍼에서 값이 1인 항목은 거울의 보이는 부분에만 해당하므로, 반사된 해골은 거울의 보이는 부분에만 렌더링된다.
5. 마지막으로 거울을 백 버퍼에 정상적으로 렌더링한다.
하지만 해골의 반사가 거울 뒤에서 보이도록 하려면, 거울을 transparency blending을 사용해 렌더링해야 한다.
거울을 투명하지 않게 렌더링하면, 거울의 깊이가 반사보다 작기 땜누에 거울이 반사를 가려버릴 것이다.
이를 구현하려면 거울에 대한 새로운 머테리얼을 정의해야 한다.
Diffuse 성분의 알파 채널을 0.5로 설정하여 거울을 50% 불투명하게 만든다.
반사된 해골 픽셀이 백 버퍼에 렌더링되었다고 가정하면, 색사의 50%는 거울에서 오고, 50%는 해골에서 온다.
앞서 설명한 알고리즘을 구현하려면 두 가지 깊이/스텐실 상태가 필요하다.
첫 번째는 거울을 그릴 때 스텐실 버퍼에서 거울 픽셀을 표시하는 데 사용된다.
두 번째는 반사된 해골을 그릴 때 사용되며, 반사된 해골이 거울의 보이는 부분에만 그려지도록 한다.
D3D11_DEPTH_STENCIL_DESC mirrorDesc;
mirrorDesc.DepthEnable = true;
mirrorDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
mirrorDesc.DepthFunc = D3D11_COMPARISON_LESS;
mirrorDesc.StencilEnable = true;
mirrorDesc.StencilReadMask = 0xff;
mirrorDesc.StencilWriteMask = 0xff;
mirrorDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
mirrorDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
mirrorDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
이것이 거울
D3D11_DEPTH_STENCIL_DESC drawReflectionDesc;
drawReflectionDesc.DepthEnable = true;
drawReflectionDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
drawReflectionDesc.DepthFunc = D3D11_COMPARISON_LESS;
drawReflectionDesc.StencilEnable = true;
drawReflectionDesc.StencilReadMask = 0xff;
drawReflectionDesc.StencilWriteMask = 0xff;
drawReflectionDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
drawReflectionDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL;
이것이 반사된 해골을 그리는 스테이트이다.
삼각형이 평면을 기준으로 반사될 떄, 그 정점 순서는 뒤집히지 않으며, 그로 인해 면의 법선 방향도 반산되지 않는다.
따라서 반사 후, 원래 빠깥을 향하고 있던 법선이 안쪽을 향하게 된다.
이를 수정하기 위해, 우리는 Direct3D에 반시계 방향으로 정점이 배열된 삼각형을 앞면으로, 시계 방향으로 배열된 삼각형을 뒷면으로 해석하도록 설정해야 한다. 이렇게 하면 법선 방향이 반사된 후에도 바깥쪽을 향하게 된다.
그러면 레스터라이저 상태를 설정하여 정점 순서 규칙을 반대로 변경한다.
D3D11_RASTERIZER_DESC cullClockwiseDesc;
ZeroMemory(&cullClockwiseDesc, sizeof(D3D11_RASTERIZER_DESC));
cullClockwiseDesc.FillMode = D3D11_FILL_SOLID;
cullClockwiseDesc.CullMode = D3D11_CULL_BACK;
cullClockwiseDesc.FrontCounterClockwise = true;
cullClockwiseDesc.DepthClipEnable = true;
ID3D11RasterizerState* CullClockwiseRS;
HR(device->CreateRasterizerState(&cullClockwiseDesc, &CullClockwiseRS));
평면 그림자 구현
그림자는 빛이장면 어디서 방출되는지 인지하는 데 도움을 주며, 결과적으로 장점을 더욱 현실적으로 만든다.
이 섹션에서 평면 그림자를 구현하는 방법을 보여준다. 즉, 평면 위에 놓인 그림자를 구현한다.
평면 그림자를 구현하려면 먼저 객체가 평면에 투영하는 그림자를 찾아 이를 기하학적으로 모델링해야 ㅎ ㅏㄴ다.
이 작업은 간단한 3D 수학으로 쉽게 수행할 수 있다.
그 후, 그림자를 설명하는 삼각형을 50% 투면도의 검은색 재질로 렌더링한다.
이렇게 그림자를 렌더링하면 "더블 블렌딩"이라는 렌더링 아티팩트가 발생할 수 있으며, 이는 이후 섹션에서 설명할 것이다. 우리는 스텐실 버퍼를 사용하여 더블 블렌딩이 발생하지 않도록 한다.
평행광 그림자
위 그림은 평행광원에 대해 객체가 투영하는 그림자를 보여준다. 평행광원의 방향을 L이라고 할 때, 정점 P를 통과하는 빛의 경로는 r(t) = p + tL로 주어진다.
광선 r(t)가 그림자 평면 (n ,d) 와 교차하는 지점이 s 이다.
객체의 각 정점에서 평면을 통과하는 광선을 통해 교차점을 찾으면, 그림자의 투영된 기하학적 형태가 정의된다. 정점 p에 대해, 그림자 투영은 다음과 같이 주어진다.
이 식은 행렬 형태로 쓸 수 있다.
이러한 4x4 행렬을 방향성 그림자 행렬이라고 부르며, Sdir 로 나타낸다.
사실 식 s는 선형 식이 아니기 때문에 행렬로 나타낼 x`수 없는데,
이전에 변환 행렬을 처리했던 것처럼 비선형 변환을 없애면 행렬 곱 형태로 나타낼 수 있다.
그러면 s' 를 얻어낼 수 있는데, s'의 w 요소에는 nㆍL 이 남아있는 상태이다.
동차 나눗셈이 발생하면 각 요소는 nㆍL로 나누어지기 때문에 온전한 s 좌표를 얻어낼 수 있다.
그림자 행렬을 사용하기 위해서는 이를 월드 행렬과 결합해야 한다.
그런데 이 경우 월드 행렬의 4행 4열 부분이 1이 아니게 되어 계산에 문제가 생길 수 있다.
그러나 현재 Vertex쉐이더에서는 4행 부분을 아예 버리고 계산하고 있기 때문에 이것은 큰 문제가 되지는 않는다.
(나는 그리 안전하게 그림자를 만드는 방식은 아니라고 생각한다.)
어쨌든... 월드 변환 후에도 여전히 기하학적 구조가 아직 그림자 평면에 투영되지 않은 상태이다. 그 이유는 원근 나누기가 아직 발생하지 않았기 때문이다.
Sw = n · L 인데, Sw < 0 이면 w-좌표가 음수가 되어 문제가 발생할 수 있다.
일반적으로 원근 투영 과정에서는 z-좌표를 w-좌표에 복사하는데, w-좌표가 음수이면 그 지점은 뷰 볼륨에 속하지 않으므로 잘려나간다. (일반적으로 동차 나누기는 클리핑 후에 발생한다.)
이는 평면 그림자에서 문제가 된다. 왜냐하면 이제 w-좌표를 사용해 그림자를 구현하면서 원근 나누기도 사용하기 때문이다.
위 그림은 nㆍL < 0 인 상황이지만 그림자는 클리핑되어 나타나지 않을 것이다.
이를 해결하려면 광선의 방향 벡터 L을 사용하는 대신, 무한히 멀리 있는 광원을 향하는 벡터 LT = -L 을 사용해야 한다.
r(t) = p + tL 과 r(t) = p + tLT 는 동일한 3D 선을 정의하며, 그 선과 평면의 교차점은 동일하다. (LT와 L 사이의 부호 차이를 보정하기 위해 교차 매개변수 ts는 다르게 계산된다.) 따라서 LT = -L 을 사용하면 동일한 결과를 얻을 수 있지만, n ㆍL > 0 이 되어 w-좌표가 음수가 되는 것을 피할 수 있다.
점 광원 그림자
위 그림은 점광원을 기준으로 물체가 드리우는 그림자를 보여준다. 점광원에서 꼭짓점 p를 통과하는 광선은 r(t) = p + t(p - L)로 주어진다. 광선 r(t)와 그림자 평면 (n, d)의 교차점은 s를 형성한다. 물체의 꼭짓점을 통과하는 광선과 평면의 교차점들을 통해 투영된 그림자의 기하하적 구조가 정의된다.
꼭짓점 p에 대한 그림자 투영은 위 식으로 주어진다.
점 광원에서는 L과 평행 광원에서의 L은 다른 의미를 가진다.
점 광원애서 L은 점광원의 위치를 정의하고 평행광원의 L은 무한히 멀리 있는 광원을 향하는 방향을 정의하는 데 사용된다.
General Shadow Matrix
동차 좌표계를 사용하면 점광원과 평행광원 모두에 적용 가능한 일반적인 그림자 행렬을 생성할 수 있따
1. Lw = 0 이면, L은 무한히 멀리 있는 광원을 향하는 방향을 나타낸다. (즉, 평행광선이 이동하는 방향의 반대 방향)
2. Lw = 1 이면, L은 점광원의 위치를 나타낸다.
이제 꼭짓점 p에서 점 s로의 변환을 다음과 같은 그림자 행렬로 나타낼 수 있다.
Lw = 0 일때는 S는 Sdir로
Lw = 1 일 때는 S는 Spoint로 축소된다는 것을 쉽게 알 수 있다.
XMFINLINE XMMATRIX XMMatrixShadow(
FXMVECTOR ShadowPlane,
FXMVECTOR LightPosition);
DirectXMath에는 위와 같은 함수를 제공한다.
이를 그림자를 표현하고자하는 메시의 월드 행렬에 곱해주면, 해당 메시에 대한 그림자를 표현할 수 있다.
이 부분을 이해가 안간다면 점 그림자, 평행 그림자 행렬을 비교해보도록 하자.
더블 블렌딩을 방지하기 위해 스텐실 버퍼 사용하기
물체의 기하학을 평면에 투영하여 그림자를 묘사할 떄, 두 개 이상의 평평한 삼각형이 겹칠 가능성이 있다. 그림자를 반투명하게 렌더링할 떄, 이러한 겹치는 영역에서 삼각형들이 여러 번 블렌딩되면 더 어둡게 나타난다.
하나의 메시인데 여러 개의 그림자가 겹쳐서 렌더링된다고 쉽게이해할 수 있다.
이 문제는 스텐실 버퍼를 사용하여 해결할 수 있다.
1. 그림자가 렌더링될 스텐실 버퍼의 픽셀이 0으로 초기화되었다고 가정한다.
2. 스텐실 테스트를 스텔실 버퍼 값이 0인 픽셀만 허용하도록 설정한다. 스텐실 테스트가 통과되면, 해당 스텐실 버퍼 값을 1로 증가시킨다.
처음 그림자 픽셀을 렌더링할 때, 스텐실 테스트는 스텐실 버퍼 값이 0이므로 통과된다. 하지만 해당 픽셀을 렌더링할 때, 스텐실 버퍼 값이 1로 증가시킨다. 따라서 이미 렌더링된 영역에 다시 그리려고 하면, 해당 스텐실 버퍼 값은 1이기 때문에 스텐실 테스트가 실패하게 된다. 이로 인해 동일한 픽셀을 여러 번 그리지 않게 되어, 이중 블렌딩을 방지한다.
위 그림에서 왼쪽 그림의 그림자 안 검은 얼룩들이 이중 블렌딩의 결과이다.
오른쪽 그림은 스텐실 버퍼를 이용하여 깔끔하게 렌더링된 그림자를 보여준다.
'언어 > C, C++' 카테고리의 다른 글
[C++] most vexing parse, 성가신 파싱 (0) | 2024.07.05 |
---|---|
[C++] 이벤트 예약 및 함수 지연 처리 구현 (1) | 2024.02.09 |
[C++] 람다 식 (Lambda) (1) | 2024.01.19 |
[C++] 스마트 포인터 - 2 (shared_ptr, weak_ptr) (0) | 2024.01.18 |
[C++] 스마트 포인터 - 1 (unique_ptr, unique_ptr 설계) (0) | 2024.01.17 |