1. 개요
이번엔 Fog를 구현하는 방법에 대해 알아보도록 하겠습니다.
아래는 Epic Games에서 제공하는 이미지입니다.
Fog를 이용하면 아주 자연스럽고 멋진 3D 세상을 구현할 수 있습니다.
2. Full Quad Rendering을 통한 구현
우선은 우리가 기본적으로 Rendering하던 방식에서 벗어나 화면을 덮는 Full Quad를 통해 전달해야 합니다.
그런데 어째서 Full Quad라는 매개체를 이용해야 하는 것일까요?
비유를 통해 그 이유를 알아보도록 합시다.
우리가 지금 쓰는 방식인 Forward Rendering은 각 객체들을 화면에 직접 그리는 방식입니다.
만약 안개를 그리고 싶다면 화면에 물체들과 안개를 같이 그려야 합니다.
그러나 이 방식보다는 우리가 그리고 싶은 물체를 우선적으로 그린 뒤 안개는 따로 그려서 겹치는 방식으로
그리는 것이 좀 더 효율적인 방식입니다.
상상해 보세요. 맑은 아크릴판에 풍경을 그린 후, 그 위에 반투명한 안개 무늬가 있는 두 번째 아크릴판을 올립니다.
이렇게 하면 전체 장면에 한 번에 일관된 안개 효과를 줄 수 있습니다.
이것이 바로 Full Quad Rendering의 원리입니다.
만약 물체마다 안개 효과를 계산하면 불필요하게 많은 연산이 발생할 수 있습니다.
대신 Full Quad를 사용하면, 렌더링된 장면 전체의 깊이 정보(Depth Buffer)를 활용하여
각 픽셀 단위로 안개 농도를 계산하게 되므로 GPU 연산이 단순화됩니다.
또한 Post Processing 단계에서 안개를 적용하면
안개의 색상, 농도, 범위 등 다양한 파라미터를 실시간으로 쉽게 변경할 수 있습니다.
3. 개발 순서
3-1. SwapChain에서 가져온 Buffer로 RTV생성 - 최종 Rendering RTV
마지막에 그려질 Backbuffer로 Render Target View 만들기 (마지막에 그려질 RTV생성)
SwapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&BackBufferTexture);
D3D11_RENDER_TARGET_VIEW_DESC FinalFrameRTVDesc = {};
FinalFrameRTVDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; // 색상 포맷
FinalFrameRTVDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; // 2D 텍스처
HRESULT hr = Device->CreateRenderTargetView(BackBufferTexture, &FinalFrameRTVDesc, &BackRTV);
if (FAILED(hr))
{
return ;
}
3-2. SceneTexture, RTV, 그리고 SRV 생성
- SceneTexture 생성:
우리가 현재 Rendering 하는 세계를 담은 Texture를 만들어야 합니다.
이 Texture에는 매 프레임 장면의 결과물이 저장될 버퍼 역할을 합니다. - Render Target View (RTV) 생성:
SceneTexture와 연결된 RTV를 생성하여, 렌더링 파이프라인이 이 텍스처에 직접 장면을 렌더링하도록 설정합니다. - Shader Resource View (SRV) 생성:
위에서 생성한 SceneTexture로부터 SRV를 생성합니다.
이 SRV는 Post Processing에서(Full Quad를 그릴 때) Shader가 SceneTexture의 내용을 Sampling할 수 있도록 하는 역할을 합니다.
https://yeoul0714.tistory.com/12 (Shader Resource View 참고)
[DX11] Shader Resource View(SRV), Sampler에 대해서
SRV (Shader Resource View) - Pixel Shader에게 어떠한 텍스처를 보내줄 것인가? - 파이프라인 과정에서 픽셀 쉐이더에 어떠한 텍스처를 넘겨줄 것인가? 기본적으로 GPU는 Raw texture 데이터를 바로 읽지 못
yeoul0714.tistory.com
바로 우리가 현재 Render 하고 있는 세계를 Shader Resource View로 뽑아내야 합니다.
그렇게 해서 나온 SRV를 Quad Rendering 시에 Texture로 넘겨주도록 할 것입니다.
SceneRenderTexture 생성하고 (ID3D11Texture2D*) 그것을 이용해 Render Target View 만들기
- 우리가 Rendering하는 세계가 담길 SRV생성
D3D11_TEXTURE2D_DESC texDesc = {};
texDesc.Width = ScreenWidth;
texDesc.Height = ScreenHeight;
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 색상 포맷
texDesc.SampleDesc.Count = 1;
texDesc.Usage = D3D11_USAGE_DEFAULT;
texDesc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
Device->CreateTexture2D(&texDesc, nullptr, &SceneRenderTexture);
texDesc.Format = DXGI_FORMAT_R32_TYPELESS; // 깊이 포맷
texDesc.BindFlags = D3D11_BIND_DEPTH_STENCIL | D3D11_BIND_SHADER_RESOURCE;
Device->CreateTexture2D(&texDesc, nullptr, &SceneRenderTexture);
Device->CreateRenderTargetView(SceneRenderTexture, nullptr, &SceneRTV);
// 셰이더 리소스 뷰 생성 (색상)
Device->CreateShaderResourceView(SceneRenderTexture, nullptr, &SceneSRV);
기본적으로 Render Target View(출력 대상) Shader Resource View(무엇을 출력할지)를 만들려면 ID3D11Texture2D가 필요합니다.
3-3. Depth Stencil View, Depth Stencil SRV 생성
- Depth Stencil Texture 생성: 3D 장면의 깊이 정보를 저장하는 전용 텍스처를 생성합니다. 이 텍스처는 각 픽셀의 카메라로부터의 거리(깊이) 정보를 저장하는 버퍼 역할을 합니다.
- Depth Stencil View (DSV) 생성: Depth Stencil Texture와 연결된 DSV를 생성하여, 렌더링 파이프라인이 3D 장면을 그릴 때 깊이 정보를 이 텍스처에 기록하고 깊이 테스트를 수행할 수 있도록 설정합니다.
- Depth Stencil SRV 생성: 위에서 생성한 Depth Stencil Texture로부터 SRV를 생성합니다. 이 SRV는 Post Processing 단계에서(Full Quad를 그릴 때) 셰이더가 DSSRV에 접근하여 안개등의 깊이 기반 효과를 계산할 수 있도록 하는 역할을 합니다.
3-4. Binding 해주기, 필요한 것들 생성(Shader)
DeviceContext->OMSetRenderTargets(1, &SceneRTV, DepthStencilView);
이런식으로 Render Target View, DepthStencilView를 Binding 해준다.
그리고 또 중요한 점은 이렇게 생성된 아이들을 마지막에 Quad에 그려줄 때에도 새로운 Shader가 필요하다는 것이다.
아래의 함수를 실행해서 Quad Shader를 생성해 준다.
void FRenderer::CreateQuadShader()
{
// 1. 쉐이더 컴파일
ID3DBlob* VertexShaderCSO = nullptr;
ID3DBlob* PixelShaderCSO = nullptr;
HRESULT hr;
// 버텍스 쉐이더 컴파일
hr = D3DCompileFromFile(L"Shaders/QuadVertexShader.hlsl", nullptr, nullptr, "VS_Main", "vs_5_0", 0, 0, &VertexShaderCSO, nullptr);
if (FAILED(hr))
{
Console::GetInstance().AddLog(LogLevel::Warning, "Quad VertexShader Compilation Error");
return;
}
// 버텍스 쉐이더 생성
hr = Graphics->Device->CreateVertexShader(
VertexShaderCSO->GetBufferPointer(),
VertexShaderCSO->GetBufferSize(),
nullptr,
&QuadVertexShader
);
if (FAILED(hr))
{
Console::GetInstance().AddLog(LogLevel::Warning, "Quad VertexShader Creation Error");
VertexShaderCSO->Release();
return;
}
// 픽셀 쉐이더 컴파일
hr = D3DCompileFromFile(L"Shaders/QuadPixelShader.hlsl", nullptr, nullptr, "PS_Main", "ps_5_0", 0, 0, &PixelShaderCSO, nullptr);
if (FAILED(hr))
{
Console::GetInstance().AddLog(LogLevel::Warning, "Quad PixelShader Compilation Error");
VertexShaderCSO->Release();
return;
}
// 픽셀 쉐이더 생성
hr = Graphics->Device->CreatePixelShader(
PixelShaderCSO->GetBufferPointer(),
PixelShaderCSO->GetBufferSize(),
nullptr,
&QuadPixelShader
);
if (FAILED(hr))
{
Console::GetInstance().AddLog(LogLevel::Warning, "Quad PixelShader Creation Error");
VertexShaderCSO->Release();
PixelShaderCSO->Release();
return;
}
// 입력 레이아웃 생성
D3D11_INPUT_ELEMENT_DESC layout[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 28, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 40, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"BLENDINDICES", 0, DXGI_FORMAT_R32_UINT, 0, 48, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
hr = Graphics->Device->CreateInputLayout(
layout,
ARRAYSIZE(layout),
VertexShaderCSO->GetBufferPointer(),
VertexShaderCSO->GetBufferSize(),
&QuadInputLayout
);
if (FAILED(hr))
{
Console::GetInstance().AddLog(LogLevel::Warning, "Quad InputLayout Creation Error");
VertexShaderCSO->Release();
PixelShaderCSO->Release();
return;
}
// 리소스 해제
VertexShaderCSO->Release();
PixelShaderCSO->Release();
}
Quad Shader는 Texture로 앞선 Render에서 넘어온 Shader Resource View, Depth Stencil SRV를 받는다.
아래는 Quad Pixel Shader 코드이다.
당장 볼 곳은 Texture2D SceneTexture, Texture2D DepthTexture이다.
여기서 Texture가 위에서 우리가 만들어준 SRV, DSSRV이다. (이곳엔 우리가 기존에 Rendering하던 World와 그 깊이값이 담겨있다.)
Render모드에 따라서 SRV를 이용해 일반적인 화면을 그리기도 하고
DSSRV를 이용해서 Depth View를 그리기도 한다.
// 입력 구조체
struct VS_OUTPUT
{
float4 Position : SV_POSITION;
float2 TexCoord : TEXCOORD0;
};
// 텍스처 및 샘플러
Texture2D SceneTexture : register(t0);
Texture2D DepthTexture : register(t1);
SamplerState SampleType : register(s0);
// 상수 버퍼 추가
cbuffer RenderModeBuffer : register(b0)
{
int RenderMode; // 0: 일반 모드, 1: 깊이 뷰 모드
float NearPlane; // 근거리 평면
float FarPlane; // 원거리 평면
float Padding; // 16바이트 정렬을 위한 패딩
};
// 픽셀 쉐이더 진입점
float4 PS_Main(VS_OUTPUT input) : SV_TARGET
{
// 깊이 텍스처에서 깊이 값 샘플링
float depth = DepthTexture.Sample(SampleType, input.TexCoord).r;
// 하드웨어 깊이값 선형화
float linearDepth = (2.0 * NearPlane * FarPlane) /
(FarPlane + NearPlane - depth * (FarPlane - NearPlane));
// 0~1 범위로 정규화 후 클램핑
float normalizedDepth = saturate((linearDepth - NearPlane) / (FarPlane - NearPlane));
// 깊이 뷰 모드인 경우
if (RenderMode == 1)
{
return float4(normalizedDepth, normalizedDepth, normalizedDepth, 1.0);
}
// 일반 모드일 경우
// 씬 텍스처에서 색상 샘플링
float4 color = SceneTexture.Sample(SampleType, input.TexCoord);
// 안개 효과 적용
return ApplyFog(color, depth, input.TexCoord);
}
4. 전체적인 Rendering 흐름
우선 if 문은 Multi View Port를 구분 해 주는 코드이고 기본적으로 지금은 단일 view port이기 때문에
else에 있는 Render만 보면 되겠다.
else에 있는 render는 우리가 1번에서 만들어줬던 RTV에 Rendering 되고 그 정보는
우리가 만들어 주었던 SRV에 저장된다.
동시에, 같은 렌더링 과정에서 3D 장면의 깊이 정보는 DepthStencilView(DSV)에 자동으로 저장됩니다.
이 깊이 정보는 원래 렌더링 과정에서 깊이 테스트를 위해 사용되지만,
우리가 만든 Depth Stencil SRV를 통해 나중에 post process 단계에서도 접근할 수 있게 됩니다.
렌더링이 완료된 후, 색상 정보(SRV)와 깊이 정보(DSSRV) 모두 Quad Shader에 전달되어
최종적인 화면 효과를 적용하는 데 사용됩니다.
void FEngineLoop::Render()
{
graphicDevice.Prepare();
if (LevelEditor->IsMultiViewport())
{
std::shared_ptr<FEditorViewportClient> viewportClient = GetLevelEditor()->GetActiveViewportClient();
for (int i = 0; i < 4; ++i)
{
LevelEditor->SetViewportClient(i);
renderer.PrepareRender(GLevel);
renderer.Render(GetLevel(),LevelEditor->GetActiveViewportClient());
}
GetLevelEditor()->SetViewportClient(viewportClient);
}
else
{
renderer.PrepareRender(GLevel);
renderer.Render(GetLevel(),LevelEditor->GetActiveViewportClient());
}
renderer.RenderToBackBuffer(LevelEditor->GetActiveViewportClient());
}
저장된 아이들은 RenderToBackBuffer에서 전부 그려지게 된다. (Post Processing)
코드를 보면 우선 RTV는 우리가 0번에서 만들어준 Backbuffer로 설정해 주고 있다.
그리고 PSSetShaderResources를 통해서 우리가 Render 한 내용이 저장되어 있는 SceneSRV, DepthSRV를
Binding 해주고 있다.
그리고 Shader도 Set 해주고 Quad 그리는데 필요한 정점, 인덱스 정보를 넘겨서 Rendering을 하고 있다
void FRenderer::RenderToBackBuffer(std::shared_ptr<FEditorViewportClient> ActiveViewport)
{
// 백버퍼를 렌더 타겟으로 설정
Graphics->DeviceContext->OMSetRenderTargets(1, &Graphics->BackRTV, nullptr);
// 렌더 모드를 constant buffer를 통해 픽셀 쉐이더에 전달
FRenderModeConstants cbData;
cbData.NearPlane = 0.1f; // 실제 Near Plane 값으로 수정
cbData.FarPlane = 100.f; // 실제 Far Plane 값으로 수정
cbData.Padding = 0.0f;
if (ActiveViewport->GetShowFlag() & static_cast<uint64>(EEngineShowFlags::Scene_Depth))
{
cbData.RenderMode = 1;
Graphics->DeviceContext->PSSetShaderResources(1, 1, &Graphics->DepthSRV);
}
else
{
cbData.RenderMode = 0;
Graphics->DeviceContext->PSSetShaderResources(0, 1, &Graphics->SceneSRV);
Graphics->DeviceContext->PSSetShaderResources(1, 1, &Graphics->DepthSRV);
}
D3D11_MAPPED_SUBRESOURCE mappedResource;
Graphics->DeviceContext->Map(RenderModeConstantBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
memcpy(mappedResource.pData, &cbData, sizeof(FRenderModeConstants));
Graphics->DeviceContext->Unmap(RenderModeConstantBuffer, 0);
Graphics->DeviceContext->PSSetConstantBuffers(0, 1, &RenderModeConstantBuffer);
// 쉐이더 설정
Graphics->DeviceContext->VSSetShader(QuadVertexShader, nullptr, 0);
Graphics->DeviceContext->PSSetShader(QuadPixelShader, nullptr, 0);
// 입력 레이아웃 설정
Graphics->DeviceContext->IASetInputLayout(QuadInputLayout);
// 정점 및 인덱스 버퍼 설정
UINT stride = sizeof(FVertexSimple);
UINT offset = 0;
Graphics->DeviceContext->IASetVertexBuffers(0, 1, &QuadVertexBuffer, &stride, &offset);
Graphics->DeviceContext->IASetIndexBuffer(QuadIndexBuffer, DXGI_FORMAT_R32_UINT, 0);
Graphics->DeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// Full Screen Quad 그리기
Graphics->DeviceContext->DrawIndexed(6, 0, 0);
}
여기까지 하면 우리는 Quad를 통해서 Rendering 하고 있는 것이 된다!!
굉장히 복잡하지만.. 사실 바로 그리던 것을 Quad를 한 번 더 통과하고 있을 뿐입니다.
원래 Back Buffer로 바로 갈 내용을 SRV, DSSRV에 담고 그것을 이용해서 Quad를 그리는 방식이라고 생각하시면 됩니다.
이렇게 SRV 와 DSSRV 로 받아야 Quad Shader에서 Post Processing이 가능해집니다.
특히 안개와 같은 효과는 깊이 정보가 필수적인데, DSSRV를 통해 깊이 정보에 접근함으로써
거리에 따른 안개 농도 변화나 높이에 따른 안개 효과 등을 계산할 수 있습니다.
Post Process의 핵심은 3D 장면을 직접 화면에 그리는 대신 중간 단계의 텍스처에 그린 후
그 결과물을 사용하여 추가적인 효과를 적용할 수 있다는 점입니다.
색상 정보와 함께 깊이 정보까지 활용함으로써 훨씬 더 풍부하고 사실적인 효과를 구현할 수 있게 됩니다.
모르시는 용어는 개념을 확실히 하시면서 읽으시면 이해가 빠르실 것이라 생각됩니다.
'DirectX11' 카테고리의 다른 글
[DX11] Point Light 구현하기 (내적, 정사영) (0) | 2025.04.09 |
---|---|
[DX11] Obj 파일 Parsing후 Pipe line Binding (2) | 2025.04.07 |
[DX11] PIE Mode, Shallow/Deep Copy (깊은 복사/ 얕은 복사) (0) | 2025.04.03 |
[DX11] Matarial Sorting을 통한 최적화 (0) | 2025.03.29 |
[DX11] Constant Buffer에 값 넘길 때 주의할 점(Shader로 Matarial값 넘길 때 주의할점) (0) | 2025.03.26 |