[DepthStencil] 거울 반사의 원리와 구현
Rasterization에서 거울 반사를 하려면 당장 떠오르는 방법은 RayTracing을 사용하는 것입니다. 생각해보면, 일반 물체를 그리고, 거울을 그린다음에 시점에서 거울로 ray를 쏘고 그 값들을 모두 gpu 즉, 쉐이더에 넣어서 계산하는 방법이 있습니다. 하지만, 복잡한 최적화 알고리즘이 필요하고 또한 느리기 때문에 보통 게임엔진에서는 사용하지 않습니다.
그렇다면, 어떤 방법이 있을까요? stencil을 이용하는 방법이 있습니다. stencil이란 쉽게 설명해서 종이에서 그리고 싶은 부분만 뚫어놓고 그 종이를 나무에 대고 스프레이를 뿌리는 방식을 생각하면 됩니다. 즉 뚫린 부분만 1로 비트마스킹하고 나머지는 0으로 만드는 개념이죠.
거울 반사의 원리 순서는 다음과 같습니다.
1. 거울이 없다고 생각하고 그냥 렌더링 한다.
=> 이 때, depth는 그대로 저장하고, stencil은 냅둔다
2. 거울을 그리고, 그려진 거울의 위치에 모두 stencil은 1을 처리해준다.
=> 이 때, 거울을 가리고 있는 물체 부분은 0으로 처리한다.(앞의 depth값 이용해서)
3. stencil이 1인 곳에 거울에 반사된 물체를 그린다.
=> 이 때 거울은 무시하고 그 거울의 위치에 물체를 그린다.
4. 거울의 위치에 거울의 색과 반사된 물체가 그려진 상을 블렌딩 한다.(섞는다)
이 때 거울 반사의 원리는 다음과 같습니다.
시점에서 거울로 ray를 쏴서 반사된 ray가 물체에 맞은 지점을 p라고 할때. p를 거울을 대칭으로 한 지점을 q라고 합니다. DirectX에서는 이 q를 구하는 행렬을 제공하는 함수가 있습니다.
autu plane = SimpleMath::Plane(Vector3(0.5f, 0.25f, 2.0f),
Vector3(-1.0f, 0.0f, 0.0f));
Matrix reflectionRow = Matrix::CreateReflection(plane);
Plane의 첫 값은 거울의 중심이며, 다음 값은 거울의 normal입니다. 왜 두 값으로 plane이 정의가 되냐면, 평면은 3차원공간에서 ax+by+cz+d =0를 생각하면 정의가 될 수 있음을 알 수 있습니다.
이 reflectionRow는 순서를 ModelRow* reflectionRow * viewRow * ProjRow로 계산합니다.
다음은 각 과정의 depthstencilState를 정의하는 과정입니다. depth와 stencil은 보통 같이 쓰이기 때문에, 그냥 하나의 API로 묶었나 봅니다.
depthSteincilView만들기
// DepthStencilView 만들기
D3D11_TEXTURE2D_DESC desc;
ZeroMemory(&desc, sizeof(desc));
desc.Width = m_screenWidth;
desc.Height = m_screenHeight;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_DEPTH_STENCIL;
desc.CPUAccessFlags = 0;
desc.MiscFlags = 0;
if (m_useMSAA && m_numQualityLevels > 0) {
desc.SampleDesc.Count = 4;
desc.SampleDesc.Quality = m_numQualityLevels - 1;
} else {
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
}
desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; //depth에 normal거리저장 24bit, stencil에 0,1저장 8bit
ThrowIfFailed(m_device->CreateTexture2D(
&desc, 0, m_depthStencilBuffer.GetAddressOf()));
ThrowIfFailed(m_device->CreateDepthStencilView(
m_depthStencilBuffer.Get(), NULL, m_depthStencilView.GetAddressOf()));
=> depth버퍼는 msaa사용한다면 같이 msaa켜줘야합니다.
desc.Format = DXGI_FORMAT_D24_UNORM_S8_UINT;인 이유는depth가 24,
8이 stencil에 사용할거기 때문에 이렇게 설정됩니다.
과정 1.
// m_drawDSS: 지금까지 사용해온 기본 DSS
D3D11_DEPTH_STENCIL_DESC dsDesc;
ZeroMemory(&dsDesc, sizeof(dsDesc));
dsDesc.DepthEnable = true;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
dsDesc.StencilEnable = false; // Stencil 불필요
dsDesc.StencilReadMask = D3D11_DEFAULT_STENCIL_READ_MASK; //=0xFF
dsDesc.StencilWriteMask = D3D11_DEFAULT_STENCIL_WRITE_MASK;//=0xFF
// 앞면에 대해서 어떻게 작동할지 설정
dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
// 뒷면에 대해 어떻게 작동할지 설정 (뒷면도 그릴 경우)
dsDesc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
dsDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
ThrowIfFailed(
m_device->CreateDepthStencilState(&dsDesc, m_drawDSS.GetAddressOf()));
=> 일반적으로 그릴 때 쓰는 방식입니다. Depth값은 저장해야하고 stencil은 아직 안쓰니 false로 해줍니다.
DepthWriteMask는 https://learn.microsoft.com/ko-kr/windows/win32/api/d3d11/ne-d3d11-d3d11_depth_write_mask
을 참고하면 좋겠습니다.
/* D3D11_DEPTH_STENCILOP_DESC 옵션 정리
* https://learn.microsoft.com/en-us/windows/win32/api/d3d11/ns-d3d11-d3d11_depth_stencilop_desc
* StencilPassOp : 둘 다 pass일 때 할 일
* StencilDepthFailOp : Stencil pass, Depth fail 일 때 할 일
* StencilFailOp : 둘 다 fail 일 때 할 일
*/
과정 2.
// Stencil에 1로 표기해주는 DSS
dsDesc.DepthEnable = true; // 이미 그려진 물체 유지
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ZERO;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
dsDesc.StencilEnable = true; // Stencil 필수
dsDesc.StencilReadMask = 0xFF; // 모든 비트 다 사용
dsDesc.StencilWriteMask = 0xFF; // 모든 비트 다 사용
// 앞면에 대해서 어떻게 작동할지 설정
dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS; //그리는 곳은 모두 stenil통과
//거울 안쪽에 포함이 되어있더라도 => stencil이 1이더라도
//거울 앞에 물체가 가리고 있다면 => depth가 실패하면
//0인 초기상태를 유지한다
//결론 : 거울을 가리지 않는 거울의 부분에만 stencil버퍼에 1을 표시해준다
ThrowIfFailed(
m_device->CreateDepthStencilState(&dsDesc, m_maskDSS.GetAddressOf()));
=> StencilFun은 DepthFunc와 비교해서 보면 좋을 것 같습니다. 그려지는 mesh는 거울이고, stencil 통과 조을 ALWAYS로 설정합니다.
m_maskDSS를 통해 거울을 그리고나면, 거울을 가리는 곳을 제외한 거울 전체에 stencil이 1이 설정됩니다.
과정 3.
// Stencil에 1로 표기된 경우에"만" 그리는 DSS => 가리지 않은 거울 부분에만 그리는 DSS
// DepthBuffer는 초기화된 상태로 가정
// D3D11_COMPARISON_EQUAL 이미 1로 표기된 경우에만 그리기
// OMSetDepthStencilState(..., 1); <- 여기의 1
dsDesc.DepthEnable = true; // 거울 속을 다시 그릴때 필요
dsDesc.StencilEnable = true; // Stencil 사용
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS_EQUAL; // <- 주의
dsDesc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_KEEP;
dsDesc.FrontFace.StencilFunc = D3D11_COMPARISON_EQUAL; //현재 stencil버퍼와 같은 픽셀만 통과하겠다
//stencil과 depth가 통과되고 stencil이 1인경우 그 픽셀은 업데이트
ThrowIfFailed(m_device->CreateDepthStencilState(
&dsDesc, m_drawMaskedDSS.GetAddressOf()));
stencil통과된 부분에만 그림을 그려줍니다. 이 때 StencilFunc이 EQUAL로 설정되어있는데, stencil데이터가 현재 stencil버퍼의 값과 같은 픽셀에만 그림을 그린다는 뜻입니다.
이 때 유의할 점은 그려질 물체가 반사되면 시계방향에서 반시계방향으로 바뀌기 때문에 이것또한 설정해줘야합니다.
과정 4.
/* "이미 그려져있는 화면"과 어떻게 섞을지를 결정
* Dest: 이미 그려져 있는 값들을 의미
* Src: 픽셀 쉐이더가 계산한 값들을 의미 (여기서는 마지막 거울)
*/
D3D11_BLEND_DESC mirrorBlendDesc;
ZeroMemory(&mirrorBlendDesc, sizeof(mirrorBlendDesc));
mirrorBlendDesc.AlphaToCoverageEnable = true; // MSAA
mirrorBlendDesc.IndependentBlendEnable = false;
// 개별 RenderTarget에 대해서 설정 (최대 8개)
mirrorBlendDesc.RenderTarget[0].BlendEnable = true;
//DestBlend가 0.7,0.7,0.7,0.7라면 SrcBlend는 0.3,0.3,0.3,0.3
//a*C+(1-a)*C에서 a와 1-a개념임
mirrorBlendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_INV_BLEND_FACTOR;
mirrorBlendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_BLEND_FACTOR;
mirrorBlendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
mirrorBlendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
mirrorBlendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
mirrorBlendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
// 필요하면 RGBA 각각에 대해서도 조절 가능
mirrorBlendDesc.RenderTarget[0].RenderTargetWriteMask =
D3D11_COLOR_WRITE_ENABLE_ALL;
ThrowIfFailed(m_device->CreateBlendState(&mirrorBlendDesc,
m_mirrorBS.GetAddressOf()));
=> 거울에 반사된 물체와 거울을 블랜딩합니다. D3D11_BLEND_BLEND_FACTOR;를 1- factor하면 D3D11_BLEND_INV_BLEND_FACTOR;입니다.
자세한 내용은 강의 코드 직접 확인.
강의 출처 : 홍정모 그래픽스 새싹코스 Part3
https://www.honglab.ai/courses/take