오브젝트에 대한 렌더링을 모두 마친 이후에, 전체 화면을 대상으로 추가적인 후처리를 적용하는 것을 포스트 프로세스라고 한다. 포스트 프로세스를 프로젝트에 구현해볼 생각이다.
먼저, 렌더링 구조를 살짝 바꿔주었다.
기존에는 오브젝트를 백버퍼에 바로 그리는 형식이었지만, 현재는 렌더타겟을 추가로 만들고 해당 렌더타겟에 오브젝트를 모두 그린 뒤에, 후처리를 적용하여 백버퍼에 복사하여 그리는 구조로 바꿔주었다.
BOOL EngineBase::CreateDoubleBuffer()
{
Microsoft::WRL::ComPtr<ID3D11Texture2D> Texture;
D3D11_TEXTURE2D_DESC txtDesc;
ZeroMemory(&txtDesc, sizeof(txtDesc));
txtDesc.Width = WindowWidth;
txtDesc.Height = WindowHeight;
txtDesc.MipLevels = txtDesc.ArraySize = 1;
txtDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT; // 이미지 처리용도
txtDesc.SampleDesc.Count = 1;
txtDesc.Usage = D3D11_USAGE_DEFAULT;
txtDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET | D3D11_BIND_UNORDERED_ACCESS;
txtDesc.MiscFlags = 0;
txtDesc.CPUAccessFlags = 0;
D3D11_RENDER_TARGET_VIEW_DESC viewDesc;
viewDesc.Format = txtDesc.Format;
viewDesc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
viewDesc.Texture2D.MipSlice = 0;
HRESULT Result;
Result = Device->CreateTexture2D(&txtDesc, NULL, Texture.GetAddressOf());
if (Result != S_OK)
{
std::cout << "CreateTexture2D() failed" << std::endl;
return FALSE;
}
Result = Device->CreateRenderTargetView(Texture.Get(), &viewDesc, DoubleBufferRTV.GetAddressOf());
if (Result != S_OK)
{
std::cout << "CreateRenderTargetView() failed" << std::endl;
return FALSE;
}
Result = Device->CreateShaderResourceView(Texture.Get(), nullptr, DoubleBufferSRV.GetAddressOf());
if (Result != S_OK)
{
std::cout << "CreateShaderResourceView() failed" << std::endl;
return FALSE;
}
return TRUE;
}
이렇게, SRV와 RTV를 모두 생성해주었고, 이 doublebuffer에 1차적인 렌더링이 모두 진행될 것이다.
다음은 그렇게 그려진 렌더링 장면에 포스트 프로세스를 적용하기 위해, 화면 크기만한 사각형 메쉬를 만들어주었다.
#include "ScreenRenderer.h"
ScreenRenderer::ScreenRenderer()
{
}
ScreenRenderer::~ScreenRenderer()
{
}
void ScreenRenderer::Init()
{
Renderer::Init();
SetModelToSquare(1.0f);
SetTransform();
}
void ScreenRenderer::Update(float _DeltaTime)
{
}
void ScreenRenderer::SetTransform()
{
TransFormData.WorldMatrix = DirectX::SimpleMath::Matrix::CreateScale(1.0f) * DirectX::SimpleMath::Matrix::CreateRotationY(0.0f) *
DirectX::SimpleMath::Matrix::CreateTranslation(DirectX::SimpleMath::Vector3(0.0f, 0.0f, 0.0f));
TransFormData.WorldMatrix = TransFormData.WorldMatrix.Transpose();
TransFormData.ViewMAtrix = EngineBase::GetInstance().ViewMat;
TransFormData.ViewMAtrix = TransFormData.ViewMAtrix.Transpose();
TransFormData.ProjMatrix =
DirectX::XMMatrixOrthographicOffCenterLH(-1.0f, 1.0f, -1.0f, 1.0f,
0.01f, 100.0f);
TransFormData.ProjMatrix = TransFormData.ProjMatrix.Transpose();
TransFormData.InvTranspose = TransFormData.WorldMatrix;
TransFormData.InvTranspose.Translation({ 0.0f, 0.0f, 0.0f });
TransFormData.InvTranspose = TransFormData.InvTranspose.Transpose().Invert();
}
다른 렌더러와 다를거 없어보이지만, 위의 렌더러는 직교투영을 적용하였다. 화면의 범위를 매쉬 크기에 맞춰주었기 떄문에 메쉬의 표면에 색을 입히면 화면 전체에 입혀지게 된다.
이 매쉬의 텍스쳐로 doublebuffer의 SRV를 사용할 것이다.
이 렌더러를 이용하여 포스트 프로세스를 적용할 것이기 때문에 포스트 프로세스 클래스도 만들어주었다.
#pragma once
#include "BaseHeader.h"
class PostProcess
{
public:
PostProcess();
~PostProcess();
PostProcess(const PostProcess& _Other) = delete;
PostProcess(PostProcess&& _Other) noexcept = delete;
PostProcess& operator=(const PostProcess& _Other) = delete;
PostProcess& operator=(PostProcess&& _Other) noexcept = delete;
void SetTexture(Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> _SRV);
virtual void Init();
virtual void Render(float _DeltaTime);
protected:
std::shared_ptr<class ScreenRenderer> PostProcessRenderer;
private:
};
각 포스트 프로세스는 위에서 만든 ScreenRenderer를 반드시 보유하도록 하였다.
이를 상속받아 여러 종류의 포스트 프로세스를 생성할 것이다.
void EngineBase::Render(float _DeltaTime)
{
float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
Context->ClearRenderTargetView(DoubleBufferRTV.Get(), clearColor);
Context->ClearDepthStencilView(DepthStencilView.Get(),
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);
Context->OMSetRenderTargets(1, DoubleBufferRTV.GetAddressOf(), DepthStencilView.Get());
Context->OMSetDepthStencilState(DepthStencilState.Get(), 0);
if (isWireFrame == false)
{
Context->RSSetState(SolidRasterizerState.Get());
}
else
{
Context->RSSetState(WireRasterizerState.Get());
}
for (std::shared_ptr<Renderer> Renderer : Renderers)
{
Renderer->Render(_DeltaTime);
}
Context->ClearRenderTargetView(BackBufferRTV.Get(), clearColor);
Context->OMSetRenderTargets(1, BackBufferRTV.GetAddressOf(), DepthStencilView.Get());
for (std::shared_ptr<PostProcess> PostProcess : PostProcesses)
{
PostProcess->SetTexture(DoubleBufferSRV);
PostProcess->Render(_DeltaTime);
}
}
엔진의 렌더링 마지막 부분에 PostProcess의 Render를 차례대로 호출해주는 것을 몰 수 있다.
포스트 프로세스 클래스는 생성할 때마다 Engine의 자료구조에 삽입되도록 하였다.
(현재는 포스트 프로세스를 적용하지 않으면 모니터에 렌더링이 안되는 괴상한 구조이다. 일단 기능을 구현하느라 이런 구조가 되었는데, 블룸을 적용한 뒤에 엔진 구조를 수정할 예정이라 일단은 그대로 두었다.)
BloomShader를 한 번 적용해볼것이다.
먼저, BloomShader는 세가지 단계로 이루어진다.
1. 밝은 부분을 추출한다.
2. 화면에 블러처리를 한다.
3. 기존의 화면에 추출된 밝은 색상을 더해준다.
그러므로 먼저 블러처리에 대한 포스트 프로세스가 잘 적용되는지부터 테스트해볼것이다.
#pragma once
#include "PostProcess.h"
struct EBloomData
{
int Width = 1600;
int Height = 900;
int Padding1 = 0;
int Padding2 = 0;
};
class BloomPostProcess : public PostProcess
{
public:
BloomPostProcess();
~BloomPostProcess();
BloomPostProcess(const BloomPostProcess& _Other) = delete;
BloomPostProcess(BloomPostProcess&& _Other) noexcept = delete;
BloomPostProcess& operator=(const BloomPostProcess& _Other) = delete;
BloomPostProcess& operator=(BloomPostProcess&& _Other) noexcept = delete;
virtual void Init() override;
virtual void Render(float _Deltatime) override;
protected:
private:
EBloomData BloomData;
};
포스트프로세스 클래스를 상속받은 BloomPostProcess 클래스를 위와 같이 만들어주었따.
#include "BloomPostProcess.h"
#include "ScreenRenderer.h"
BloomPostProcess::BloomPostProcess()
{
}
BloomPostProcess::~BloomPostProcess()
{
}
void BloomPostProcess::Init()
{
PostProcessRenderer = std::make_shared<ScreenRenderer>();
PostProcessRenderer->Init();
PostProcessRenderer->CreateConstantBuffer(EShaderType::PSShader, L"BloomPixelShader.hlsl", BloomData);
PostProcessRenderer->SetVSShader(L"BloomVertexShader.hlsl");
PostProcessRenderer->SetPSShader(L"BloomPixelShader.hlsl");
PostProcessRenderer->SetSampler("LINEARWRAP");
}
void BloomPostProcess::Render(float _DeltaTime)
{
PostProcessRenderer->Render(_DeltaTime);
ID3D11ShaderResourceView* SRV = NULL;
EngineBase::GetInstance().GetContext()->PSSetShaderResources(0, 1, &SRV);
}
초기화는 위와 같다. 렌더러를 먼저 만들어준뒤, 상수버퍼를 연결해주고 쉐이더 세팅을 해주었다.
그 이후, Render함수에선 렌더링을 진행한 뒤, SRV를 비워주도록 하였다.
#include "LightHeader.hlsli"
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float2 TexCoord : TEXCOORD;
};
cbuffer EBloomData : register(b0)
{
int Width;
int Height;
int Padding1;
int Padding2;
};
Texture2D DiffuseTexture : register(t0);
SamplerState Sampler : register(s0);
static float Gau[5][5] =
{
{ 1, 4, 6, 4, 1 },
{ 4, 16, 24, 16, 4 },
{ 6, 24, 36, 24, 6 },
{ 4, 16, 24, 16, 4 },
{ 1, 4, 6, 4, 1 }
};
float4 main(PixelShaderInput _Input) : SV_TARGET
{
float4 Color = (float4)0.0f;
float WidthRatio = 1.0f / (float) Width;
float HeightRatio = 1.0f / (float) Height;
float2 StartTexCoord = float2(_Input.TexCoord.x - WidthRatio * 2, _Input.TexCoord.y - HeightRatio * 2);
for (int i = 0; i < 5; i++)
{
for (int j = 0; j < 5; j++)
{
StartTexCoord.y += HeightRatio;
Color += DiffuseTexture.Sample(Sampler, StartTexCoord.xy) * Gau[j][i];
}
StartTexCoord.x += WidthRatio;
StartTexCoord.y = _Input.TexCoord.y;
}
Color /= 256.0f;
return Color;
}
위는 블러를 적용하는 픽셀쉐이더 코드이다.
전형적인 가우시안 블러 코드를 그대로 구현하였다.
좌측이 기본이고 우측이 블러를 적용한 상태이다.
다행히 잘 작동되는 듯 하다.
다음엔 이걸 토대로 더 많은 렌더타겟을 사용하여 블룸효과를 완전히 구현할 것이다.
그 다음엔 엔진 구조를 다소 개편해보고자 한다.