DirectX 11을 이용한 3D게임 프로그래밍 입문 연습문제 10
스텐실 버퍼는 후면버퍼의 특정 영역여 픽셀을 그려줄지 말지 결정하는 버퍼입니다. 그리고 후면 버퍼에 그릴지 말지를 판정하는 걸 스텐실 판정이라고 합니다.
스텐실 판정의 의사코드는 아래와 같습니다.
if(StencilRef & StencilReadMask 연산자 Value & StencilReadMask)
픽셀 허용
else
픽셀 폐기
StencilRef는 OMSetDepthStencilState메서드 호출할때 프로그래머가 직접 지정할 수 있는 값입니다.
StencilReadMask또한 구조체를 통해 프로그래머가 직접 지정할 수 있습니다.
Value는 스텐실 버퍼에 있던 해당 픽셀의 값입니다.
스탠실버퍼를 정의하기 위해선 구조체 내용을 채워야합니다. 처음 본 사람이라면 한번에 이해하기 힘드니 여러번 보면 좋습니다. 하나하나 살펴보도록 하겠습니다.
구조체안에 깊이와 스텐실 모두 정의할 수 있습니다.
깊이관련부터 살펴보겠습니다.
DepthEnable은 깊이 버퍼를 활성화할지 여부를 결정하는 변수입니다. 만약 false로 지정한다면 깊이 판정 없이 그리는 순서대로 화면에 나타나게 됩니다.
DepthWriteMask는 D3D11_DEPTH_WRITE_ZERO or ALL로 나뉩니다. ZERO는 깊이 버퍼를 갱신하지 않고 대신 판정은 계속합니다. ALL은 깊이버퍼와 스텐실버퍼를 모두 통과한 픽셀의 깊이가 깊이버퍼에 새로 갱신됩니다.
DepthFunc는 깊이 판정을 수행할 함수를 지정합니다. 기본값은 LESS이며 ALWAYS나 GREATER, NOT EQUAL 등으로 설정할 수 있다. 비교 함수가 LESS라면 가장 가까운 픽셀이 화면에 그려지게 되고 먼 픽셀은 폐기된다.
StencilEnable은 단어 그대로 스텐실을 활성화 할건지 안할건지를 결정합니다.
StencilReadMask는 위 의사코드에서 쓰인 StencilReadMask와 같은겁니다. 여기서 프로그래머가 직접 값을 지정할 수 있습니다. 0xff로 지정하면 and비트연산자이기 때문에 모든 비트가 통과됩니다.
StenWriteMask는 스탠실 버퍼의 특정 비트 값들이 갱신되지 않도록 하는 비트 마스크이다. 기본값은 어떤 비트도 막지 않는 마스크이다.(0xff)
FrontFace와 BackFace는 D3D11_DEPTH_STENCILOP_DESC이며 이것은 스텐실판정, 깊이판정의 실패성공여부에 따라 스텐실버퍼의 값을 갱신하는 역할을 한다.
StencilFailOp는 픽셀이 스텐실 판정에 실패했을 때 값을 어떻게 변경할건지 방식을 결정하는 변수. 자주쓰는것만 설명하자면 KEEP은 스텐실 값을 변경하지 않고 유지하는 것이고, ZERO는 0으로 초기화, REPLACE는 StencilRef값으로 설정. OMSetDepthStencil메서드를 호출할때 지정할 수 있다.
StencilDepthFailOp는 스텐실 판정은 통과했지만 깊이 판정에 실패했을때의 방식을 결정한다.
StencilPassOp는 스텐실과 깊이 판정 모두 통과했을때의 방식을 결정한다.
StencilFunc는 스텐실 판정 비교 함수이다. DepthFunc와 같은 구조체입니다.
문제 3
그림 10.1의 왼쪽 모습이 나오도록 거울 예제를 수정하라.
해결
사진이 흐려서 잘 보이진 않지만 아마 거울임에도 불구하고 벽 너머로 반사상이 보이는 현상을 보여준것같습니다.
조금만 생각하면 간단합니다. 이 현상은 스텐실 버퍼에 아무런 간섭을 하지 않아 이렇게 된것이다. 그냥 유리입니다 저스트 글라쑤.
그러니까 반사상을 그릴때 OMSetDepthStencilState를 설정하지 않으면 끝입니다. 왜인지 하나씩 따져보기 전에 거울을 어떻게 구현하는지 설명하겠습니다.
위 소스는 거울에 대한 스텐실 버퍼를 갱신할때의 소스코드이다. 여기서 흥미로운 점은 오직 스텐실 버퍼만을 갱신하며 화면에 그리는것은 일절 없다.
이를 가능케 한건 위와 같은 혼합상태 때문이다. 사실 이게 혼합과 연관이 있는건지는 잘 모르겠지만 RenderTargetWriteMask를 0으로 지정했다. 그러면 모든 비트가 0이기 때문에 red,blue,green,alpha채널이 전부 꺼지므로 아무것도 그려지지 않게 된다.
다음은 깊이 스텐실 버퍼 상태이다. FrontFace를 살펴보면 깊이와 스텐실 둘중 하나라도 판정이 실패되면 값은 변하지 않지만 모두 통과했을때 stencilref로 스텐실 버퍼에 값을 설정하는 형태이다. 참고로 여기서 stencilref는 1이다.
간단하게 생각해서 본래 후면버퍼에 거울이 그려지는 대신에 스텐실버퍼에 거울이 그려지는 부분이 1로 채워진다고 생각하면 된다.
다음은 반사상 그리기입니다.
xy평면을 기준으로 반사상을 그리게 하기 위해서는 벡터 하나를 정의해야합니다. z를 제외한 모든 부분을 0으로 정의합니다. w값으로 반사상과의 거리를 어느정도 조절이 가능합니다. 그리고 xmmatrixreflect로 반사행렬을 만듭니다. mirrorPlane값을 바탕으로 반사행렬을 정의해줍니다.
그리고 기존 SRT에 R(Reflect)행렬을 추가로 곱해줍니다.(이게 되네요..?) 그러면 해골의 위치는 SRTR행렬에 의해 반사위치가 정해집니다.
반사상의 SRTR은 정의가 완료되었습니다. 하지만 빛 또한 반사되어야합니다. 아래의 왼쪽사진이 빛을 반사시키지 않았을때의 모습입니다. 바닥 위에있는 해골의 거울(후면선별해서 안보임)과 마주보고있는 면은 빛을 잘 받고있지않기때문에 어두운 명암을 띄고있습니다. 그렇다면 거울에 보이는 해골 또한 어두운 면이 보여야합니다. 아래의 왼쪽사진은 빛을 반사시키지 않았기 때문에 거울에서 봤을 때 밝은 면을 보여주는 왜곡된 모습을 보여줄겁니다. 그러므로 빛을 반사시켜 오른쪽 아래와 같이 어두운 면이 거울을 통해 보이도록 해야합니다.
XMVector3TransformNormal은 함수명은 복잡하지만 내용은 그저 벡터와 행렬의 곱입니다. 전에 구했던 반사행렬을 이용해 반사된 빛을 구하고 적용해주면 됩니다.
이제 반사상을 그립니다. 여기서 특별히 주의해야할 점이 있습니다. 바로 레스터라이즈상태를 적용해야 한다는 것입니다. 삼각형을 그릴땐 시계방향으로 그리는데 이를 전면으로 판단합니다. 하지만 반사상에 그려진 삼각형은 반시계방향이기때문에 후면선별에 의해 화면에 그려지지 않게됩니다. 그래서 레스터라이즈를 통해 FrontCounterClockwise를 true로 설정해줍니다. 이 변수는 후면선별을 끄는것이 아닌 반시계방향을 전면으로 간주한다는 뜻입니다. 그리고 이러한 역할을 해주는것이 RenderStates::CullClockwiseRS입니다.
이제 정점이 반시계방향이여도 뷰포트에 보이도록 수정하였습니다. 그다음은 오직 거울에만 반사상이 그려지도록 해야합니다. 좀전에 거울의 스텐실버퍼를 드로잉 했습니다. 거울에 그려질 값들이 전부 1로 채워져있는데 반사상을 그릴때 픽셀ij번째가 스텐실버퍼 픽셀ij번째의 값이 1일때만 그려지도록 해야합니다. 그래서 D3D11_COMPARISON_EQUAL 스텐실 비교 함수를 사용합니다.
반사상을 그렸을때 나타난 결과화면입니다. 다시 본래의 문제로 돌아가자면 해골이 스텐실 버퍼에 의존하지 않고 평범하게 출력하게 하면 되는 문제였는데요. 위에서 말했듯이 반사상은 거울의 스텐실 버퍼를 드로잉할때의 데이터를 기준으로 픽셀별 값을 비교하며 출력여부를 결정합니다. 이 과정을 없애면 문제를 풀수있죠. 그래서 그저 반사상을 그릴 때OMSetDepthStencilState 함수를 호출하지 않으면 그만입니다.
문제 4
그림 10.10의 왼쪽 모습이 나오도록 거울 예제를 수정하라.
해결
우선 의도치않게 이중혼합현상이 왜 일어나는지 알아보겠습니다.
우선 그림자를 그릴때 직선의 방정식을 이용해 정점을 기준으로 빛벡터와 법선벡터를 바탕으로 그림자의 정점을 구합니다.(자세한 설명은 추후 따로 포스팅하겠습니다.) 그리고 그 정점들을 기준으로 그림자가 그려지고요. 하지만 그림자 정점들을 바탕으로 삼각형을 그리다보면 겹칠 때가 있습니다. 그림자는 기본적으로 반투명이기때문에 삼각형이 겹치게되면 이중혼합현상이 일어나게됩니다. 그렇다면 혼합을 끄면 되지 않을까?라는 생각을 잠깐 가지게되었는데 바닥텍스쳐와 혼합이 이루어져야 하므로 이는 좋은방법이 아니였습니다. 그렇다면 스텐실버퍼를 이용해 이미 그려진 곳은 더이상 그려지지않도록 하는 방식을 이용해야합니다.
D3D11_STENCIL_OP_INCR는 스텐실 값을 1씩 증가시키며 그 값은 clamp됩니다. 여기서 말하는 clamp는 간단하게 최솟값보다 작으면 최솟값, 최대값보다 크면 최대값으로 값이 한정된다는 뜻입니다. 스텐실과 깊이판정을 통과한 스텐실버퍼 픽셀ij는 값이 1씩 오르게 되며 만약 이중혼합이라면 그 픽셀의 값이 1이상일때 기각하면 됩니다. 문제가 이중혼합현상을 유도하는 거였으니 이 기능을 사용하지 않으면 됩니다.
문제 5
거울 예제를 다음과 같은 깊이 설정을 적용해서 렌더링한다.
wallBlendDesc.DepthEnable = false;
wallBlendDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
wallBlendDesc.DepthFunc = D3D11_COMPARISON_LESS;
다음으로 벽 뒤의 두개골을 다음과 같은 깊이 설정을 적용해서 렌더링한다.
noStencilDrawReflectDesc.DepthEnable = false;
noStencilDrawReflectDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
noStencilDrawReflectDesc.DepthFunc = D3D11_COMPARISON_LESS;
이렇게하면 두개골이 벽에 가려질까? 결과를 설명하라. 벽을 다음과 같은 설정으로 렌더링하면 어떻게 될까?
noStencilDrawReflectDesc.DepthEnable = true;
noStencilDrawReflectDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
noStencilDrawReflectDesc.DepthFunc = D3D11_COMPARISON_LESS;
이 연습문제는 스텐실 버퍼를 사용하지 않으므로 스텐실 버퍼를 비활성화해야 함을 주의할 것.
해결
문제에 소스코드가 나와있으니 결과화면만 보도록 하겠습니다.
우선 반사상의 안면이 붕괴됬는데 깊이판정이 일어나지 않아서 생긴 결과로 추측된다. 우선 벽과 반사상 둘다 깊이판정을 껐기 때문에 가장 나중에 그려진 반사상이 항상 어느각도든 벽을 가려서 그려지게된다. 즉 카메라에 뭐가 가장 가까이 있냐를 따지는게 아닌 가장 나중에 그려지는게 맨앞에있는것처럼 보이게된다. 이 사실을 뒷받침해주듯이 오른쪽 사진을 자세히보면 반사상이 실제 해골보다 나중에 그려지게 되므로 본래 해골에 가려져서 보이지 않아야 할 반사상이 우리 눈에 보이게 된다.
반사상의 깊이판정을 활성화했더니 원래대로 돌아왔다. 하지만 벽은 깊이판정을 끈 상태이기때문에 벽의 깊이값을 무시하고 반사상이 그대로 보이게 된다. 하지만 오른쪽사진을 보면 깊이 판정에 의해 본체 해골에 가려지게 되어 더이상 반사상이 보이지 않게된다.
문제 6
거울 반사상을 렌더링할 때 삼각형 감기 순서를 통상적인 관례의 반대로 설정하지 않도록 거울 예제를 수정하라. 반사된 두개골이 제대로 나타나는가?
위에서도 설명했지만 다시 말하자면 반사된 정점들은 위치도 반사되기 때문에 정점이 감기는 순서가 반대방향으로 바뀌게된다. 그래서 레스터라이즈화를 통해 반시계방향이 후면이 아닌 전면으로 판단하게 해야한다.
문제 7
제9장의 혼합 예제('BlendDemo')를 다음과 같이 수정하라. 장면 중앙에 원기둥 하나를 그린다(꼭대기의 구 없이 원기둥 하나만). 웹 부록의 이번 장 예제 디렉터리에 있는 60프레임짜리 번개 애니메이션을 가산 혼합을 이용해서 원기둥에 텍스처로 입힌다.
해결
bmp파일적용과 애니메이션에 대한 설명은 아래 링크 참고부탁드립니다.
https://wdmab1204.tistory.com/10
가산혼합은 원본혼합계수와 대상혼합계수 모두 (1,1,1)로 정의하고 연산자는 add로 하면 됩니다.
문제 8
제 9장의 혼합 예제에 쓰인 장면의 깊이 복잡도를 렌더링하라.
해결
우선 거울예제에서 스탠실버퍼를 그리듯이 이 BlendDemo예제의 철망과 산, 그리고 물을 그려줍니다. 이때 스텐실 연산은 incr로 해주어야 픽셀마다 얼마나 깊이 복잡도가 측정되는지 알수있습니다. 물론 StencilFunc는 Always입니다.
깊이싸움이 심할수록 복잡도의 값이 큽니다. 이를 시각화하기 위해 정가운데에 넓고 납작한 큐브를 생성합니다. 스텐실 버퍼에 의거하여 깊이 복잡도에 따라 Material을 다르게 해준다면 이를 눈으로 확인할 수 있습니다.
문제 9
가산 혼합을 이용해서 깊이 복잡도를 시각화 하라.
해결
책에 적혀있는 문제 자체에 이미 풀이과정이 나와있습니다. 주의할점이라면 렌더상태를 0으로 초기화하는 것도 없애야 합니다. 그리고 후면버퍼를 지우는 것은 ClearRenderTargetView를 호출하는것을 의미하며 이미 이것은 실행되고있습니다. 그리고 깊이판정을 비활성화 하는것은 depthenable을 false로해주시면 됩니다.
문제 10
깊이 판정을 통과한 픽셀들의 개수를 세는 방법을 설명하라. 깊이 판정에 실패한 픽셀들의 개수를 세는 방법을 설명하라.
해결
D3D11_DEPTH_STENCIL_DESC 구조체 멤버중에 FrontFace와 BackFace의 StencilPassOp라는 변수가있다. 이는 스텐실 판정과 깊이 판정을 모두 통과했을때 스텐실버퍼에 어떻게 값을 넣을건지 결정하는 변수이다. 스텐실 판정을 Always로 하고 깊이 판정만을 LESS로 설정해주면 스텐실버퍼를 0으로 clear했다고 가정할때 0보다 큰 값을 가진 픽셀들은 깊이 판정에 최종적으로 성공해 우리 눈에 보이는 픽셀들이다.
깊이 판정에 실패한 픽셀 개수를 세는 방법도 위와 비슷하다. FrontFace와 BackFace의 StencilDepthFailOp는 스텐실 판정에 성공하였으나 깊이 판정에 실패했을때 스텐실 버퍼에 어떻게 값을 넣을건지 결정하는 변수이다. 위와같은 방법으로 스텐실 버퍼에 값을 넣어 초기화 한 값과 다르다면 그 픽셀은 깊이판정에 실패한 픽셀이다.
문제 11
바닥을 거울로 구현하여 반사상을 그려라.
해결
거울이 어떻게 구현되는지 이해하고있다면 쉽게 풀 수 있다.
바닥이 렌더링되지않게 BlendState를 설정하고 스텐실버퍼를 그린다.
반사상을 그린다. 기존의 반사상은 xy평면을 기준으로 반사되었으니 이번은 바닥이므로 xz평면을 기준으로 반사상을 그린다.
그리고 반투명한 바닥을 그린다.
기존 바닥을 그리는 소스코드에 NoRenderTargetWriteBS와 MarkMirrorDSS를 추가했다.
NoRenderTargetWirteBS는 back buffer에 아무것도 그리지 않는 blend state이며
MarkMirrorDSS는 스텐실버퍼의 본래 그려질 픽셀위치에 값을 넣는다.
소스코드는 길지만 기존에 그리던 반사상과 똑같다. 바뀐점은 xz평면을 기준으로 반사상이 그려지기 때문에 mirrorPlane을 (0,1,0,0)으로 설정하였다.
기존 material은 mRoomMat을 썼지만 반투명효과를 내기위해 거울과 똑같은 머터리얼로 사용하였다.
그리고 반투명효과를 적용하기위해 TransparentBS를 설정했다.
혼란스럽다. 하지만 원인을 찾았습니다. 벽의 거울의 스텐실버퍼와 바닥의 스텐실버퍼들은 전부 1이라는 값으로 설정되어있습니다. 그래서 바닥의 반사상이 벽거울의 스텐실판정에 통과됨으로써 위와같은 현상이 나타났습니다.
지금 제 상식선에서 알아낸 해결방법은 거울을 다 그릴때마다 스텐실버퍼를 초기화하거나 거울마다 스텐실 값을 다르게 하는 방법이 떠오릅니다. 더 좋은 방법이있다면 추가로 작성하겠습니다.
바닥거울의 스텐실값을 2로 설정해서 실행해보겠습니다.
이렇게 해서 제10장의 대부분 문제를 풀었습니다. 갠적으로 가장 재밌게 공부한 챕터인것같습니다. 직각인 거울만 구현을 했는데 기울어진 거울이나 곡면의 거울은 나중에 어떻게 구현하는지 궁금하기도 합니다. 다음 11장 연습풀이에서 뵙도록 하겠습니다.