[목차]
- 포스트 프로세싱이란
- 포스트 프로세싱 적용해보기
[포스트 프로세싱이란]
포스트 프로세싱은 3D 그래픽스에서 렌더링이 완료된 화면 이미지를 후처리하여 다양한 시각적 효과를 추가하는 단계를 말합니다. RenderTarget에 그려진 결과물(주로 G-Buffer)에 대해 추가로 이미지 필터를 적용하는 것입니다. 다양한 효과들이 있습니다.
- 블름(블러)
- 화면 전체 색감 보정
- 피사계 심도
- HDR 톤 매핑
- 안티 앨리어싱(FXAA)
- 디스토션
[포스트 프로세싱 적용해보기]
이제 프레임워크에 실제로 포스트 프로세싱을 적용해보겠습니다. 먼저 PostProcess 인터페이스를 담당하는 클래스를 만들어줍니다.
#pragma once
#include "Ext_DirectXRenderTarget.h"
class Ext_PostProcess : std::enable_shared_from_this<Ext_PostProcess>
{
friend class Ext_DirectXRenderTarget;
public:
std::shared_ptr<Ext_DirectXRenderTarget> GetPostTarget() { return PostTarget; }
protected:
struct FrameData
{
float4 ScreenSize; // 화면(또는 텍스처) 크기
float AccTime = 0.0f; // 실행된 시간(초)
};
virtual void Start() = 0;
virtual void PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime) = 0;
std::shared_ptr<Ext_MeshComponentUnit> PostUnit;
std::shared_ptr<Ext_DirectXRenderTarget> PostTarget;
FrameData FData;
private:
};
해당 인터페이스를 상속받아서 만들어지는 PostProcess들은 RenderTarget이 들고 있도록 해줍니다.
///////////// Ext_DirectXRenderTarget.h
//...
public:
// 포스트 프로세스 만들기
template<typename PostType>
std::shared_ptr<PostType> CreateEffect()
{
std::shared_ptr<PostType> NewPostProcess = std::make_shared<PostType>();
PostProcessInitialize(NewPostProcess);
PostProcesses.push_back(NewPostProcess);
return NewPostProcess;
}
void Ext_DirectXRenderTarget::PostProcessing(std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
for (size_t i = 0; i < PostProcesses.size(); i++)
{
PostProcesses[i]->PostProcessing(this, _Camera, _DeltaTime);
}
}
std::vector<std::shared_ptr<class Ext_PostProcess>>& GetPostProcesses() { return PostProcesses; }
private:
std::vector<std::shared_ptr<class Ext_PostProcess>> PostProcesses = {};
이렇게 RenderTarget에 대해 PostProcess를 추가하면 컨테이너 요소를 순회하면서 Update()가 진행될 것입니다.
// 카메라 생성 시 호출
void Ext_Camera::Start()
{
ViewPortData.TopLeftX = 0;
ViewPortData.TopLeftY = 0;
ViewPortData.Width = Base_Windows::GetScreenSize().x;
ViewPortData.Height = Base_Windows::GetScreenSize().y;
ViewPortData.MinDepth = 0.0f;
ViewPortData.MaxDepth = 1.0f;
Width = ViewPortData.Width;
Height = ViewPortData.Height;
// 카메라 최종 렌더타겟
CameraRenderTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL); // 해당 카메라의 최종 결과물 타겟
CameraRenderTarget->CreateDepthTexture();
// 추가할 PostProcess들
// CameraRenderTarget->CreateEffect<Ext_Blur>();
// CameraRenderTarget->CreateEffect<Ext_Distortion>();
// CameraRenderTarget->CreateEffect<Ext_OldFilm>();
// CameraRenderTarget->CreateEffect<Ext_TextureTest>();
// ...
}
// 카메라의 MeshComponents들에 대한 업데이트 및 렌더링 파이프라인 리소스 정렬
void Ext_Camera::Rendering(float _Deltatime)
{
// ...
// 모든 렌더링 과정이 끝난 후 실시해준다.
CameraRenderTarget->PostProcessing(GetSharedFromThis<Ext_Camera>(), _Deltatime);
}
0. 렌더링 파이프라인 세팅
: Vertex Shader, Pixel Shader만 다르고 Blend State, Depth State, Rasterzier State, Sampler State는 모두 같습니다.
std::shared_ptr<Ext_DirectXMaterial> NewRenderingPipeline = Ext_DirectXMaterial::CreateMaterial("XXXEffect");
NewRenderingPipeline->SetVertexShader("XXXEffect_VS");
NewRenderingPipeline->SetPixelShader("XXXEffect_PS");
NewRenderingPipeline->SetBlendState("BaseBlend");
NewRenderingPipeline->SetDepthState("AlwayDepth");
NewRenderingPipeline->SetRasterizer("NonCullingRasterizer");
// Blend State : 알파 블렌드
D3D11_BLEND_DESC BlendInfo = { 0, };
BlendInfo.AlphaToCoverageEnable = false;
BlendInfo.IndependentBlendEnable = false;
BlendInfo.RenderTarget[0].BlendEnable = true;
BlendInfo.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; // 자주 쓰는 조합 1
BlendInfo.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
BlendInfo.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
BlendInfo.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ONE;
BlendInfo.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
BlendInfo.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
// Depth State : 깊이 테스트 진행 X
D3D11_DEPTH_STENCIL_DESC DepthStencilInfo = { 0, };
DepthStencilInfo.DepthEnable = true;
DepthStencilInfo.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
DepthStencilInfo.DepthFunc = D3D11_COMPARISON_ALWAYS;
DepthStencilInfo.StencilEnable = false;
// Rasterizer State : Back-FaceCuliiing 안하는 Rasterizer
D3D11_RASTERIZER_DESC Desc = {};
Desc.CullMode = D3D11_CULL_NONE;
Desc.FrontCounterClockwise = FALSE;
Desc.FillMode = D3D11_FILL_SOLID;
1. Blur(Bloom)
: 가우시안 블러를 사용했습니다.
#include "PrecompileHeader.h"
#include "Ext_Blur.h"
#include <DirectX11_Base/Base_Windows.h>
#include "Ext_MeshComponentUnit.h"
void Ext_Blur::Start()
{
PostUnit = std::make_shared<Ext_MeshComponentUnit>();
PostUnit->MeshComponentUnitInitialize("FullRect", "Blur");
PostTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL);
}
void Ext_Blur::PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
PostTarget->RenderTargetClear();
PostTarget->RenderTargetSetting();
FData.ScreenSize = Base_Windows::GetScreenSize();
PostUnit->GetBufferSetter().SetConstantBufferLink("FrameData", FData);
PostUnit->GetBufferSetter().SetTexture(_MainRenderTarget->GetTexture(0), "DiffuseTex");
PostUnit->Rendering(_DeltaTime);
PostUnit->GetBufferSetter().AllTextureResourceReset();
_MainRenderTarget->RenderTargetClear();
_MainRenderTarget->Merge(PostTarget);
}
// 가우시안 커널
// Total = 16+64+96+64+16 = 256
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 }
};
// 7×7 가우시안 커널 (Pascal row 6: 1, 6, 15, 20, 15, 6, 1)
// 전체 가중치 합 = (1+6+15+20+15+6+1)² = 64² = 4096
static const float Gau7[7][7] =
{
{ 1, 6, 15, 20, 15, 6, 1 },
{ 6, 36, 90, 120, 90, 36, 6 },
{ 15, 90, 225, 300, 225, 90, 15 },
{ 20, 120, 300, 400, 300, 120, 20 },
{ 15, 90, 225, 300, 225, 90, 15 },
{ 6, 36, 90, 120, 90, 36, 6 },
{ 1, 6, 15, 20, 15, 6, 1 }
};
// 9×9 가우시안 커널 (Pascal row 8: 1, 8, 28, 56, 70, 56, 28, 8, 1)
// 전체 가중치 합 = (1+8+28+56+70+56+28+8+1)² = 256² = 65536
static const float Gau9[9][9] =
{
{ 1, 8, 28, 56, 70, 56, 28, 8, 1 },
{ 8, 64, 224, 448, 560, 448, 224, 64, 8 },
{ 28, 224, 784, 1568, 1960, 1568, 784, 224, 28 },
{ 56, 448, 1568, 3136, 3920, 3136, 1568, 448, 56 },
{ 70, 560, 1960, 3920, 4900, 3920, 1960, 560, 70 },
{ 56, 448, 1568, 3136, 3920, 3136, 1568, 448, 56 },
{ 28, 224, 784, 1568, 1960, 1568, 784, 224, 28 },
{ 8, 64, 224, 448, 560, 448, 224, 64, 8 },
{ 1, 8, 28, 56, 70, 56, 28, 8, 1 }
};
cbuffer FrameData : register(b0)
{
float4 ScreenSize; // 화면 크기
float AccTime; // 실행된 시간
};
Texture2D DiffuseTex : register(t0);
SamplerState Sampler : register(s0);
struct PSInput
{
float4 Position : SV_POSITION;
float2 Texcoord : TEXCOORD;
};
// 5x5 가우시안 블러
//float4 Blur_PS(PSInput _Input) : SV_TARGET
//{
// // 픽셀 하나 사이즈는 몇입니까
// float2 PixelSize = float2(1.0f / ScreenSize.x, 1.0f / ScreenSize.y);
// // UV 중심에서 2픽셀씩 왼쪽위로 이동하여 가우시안 시작 지점 정하기
// float2 PixelUvCenter = _Input.Texcoord.xy;
// float2 StartUV = _Input.Texcoord.xy + (-PixelSize * 2.0f);
// float2 CurUV = StartUV;
// float4 ResultColor = (float4) 0.0f;
// // 5×5 샘플링 & 가중치 누적
// for (int y = 0; y < 5; ++y)
// {
// for (int x = 0; x < 5; ++x)
// {
// ResultColor += DiffuseTex.Sample(Sampler, CurUV.xy) * Gau[y][x];
// CurUV.x += PixelSize.x;
// }
// CurUV.x = StartUV.x;
// CurUV.y += PixelSize.y;
// }
// // 가중치 총합 256 으로 정규화
// // 이렇게 해야 모든 샘플 값이 커널 가중치만큼 더해져서 전체 이미지가 지나치게 밝아지거나 어두워지는 것 방지
// ResultColor /= 256.0f;
// return ResultColor;
//}
// 7x7 가우시안 블러
//float4 Blur_PS(PSInput _Input) : SV_TARGET
//{
// // 픽셀 하나 사이즈는 몇입니까
// float2 PixelSize = float2(1.0f / ScreenSize.x, 1.0f / ScreenSize.y);
// // UV 중심에서 2픽셀씩 왼쪽위로 이동하여 가우시안 시작 지점 정하기
// float2 PixelUvCenter = _Input.Texcoord.xy;
// float2 StartUV = _Input.Texcoord.xy + (-PixelSize * 3.0f);
// float2 CurUV = StartUV;
// float4 ResultColor = (float4) 0.0f;
// // 7×7 샘플링 & 가중치 누적
// for (int y = 0; y < 7; ++y)
// {
// for (int x = 0; x < 7; ++x)
// {
// ResultColor += DiffuseTex.Sample(Sampler, CurUV.xy) * Gau7[y][x];
// CurUV.x += PixelSize.x;
// }
// CurUV.x = StartUV.x;
// CurUV.y += PixelSize.y;
// }
// // 가중치 총합 256 으로 정규화
// // 이렇게 해야 모든 샘플 값이 커널 가중치만큼 더해져서 전체 이미지가 지나치게 밝아지거나 어두워지는 것 방지
// ResultColor /= 4096.0f;
// return ResultColor;
//}
// 9x9 가우시안 블러
float4 Blur_PS(PSInput _Input) : SV_TARGET
{
// 픽셀 하나 사이즈는 몇입니까
float2 PixelSize = float2(1.0f / ScreenSize.x, 1.0f / ScreenSize.y);
// UV 중심에서 2픽셀씩 왼쪽위로 이동하여 가우시안 시작 지점 정하기
float2 PixelUvCenter = _Input.Texcoord.xy;
float2 StartUV = _Input.Texcoord.xy + (-PixelSize * 4.0f);
float2 CurUV = StartUV;
float4 ResultColor = (float4) 0.0f;
// 7×7 샘플링 & 가중치 누적
for (int y = 0; y < 9; ++y)
{
for (int x = 0; x < 9; ++x)
{
ResultColor += DiffuseTex.Sample(Sampler, CurUV.xy) * Gau9[y][x];
CurUV.x += PixelSize.x;
}
CurUV.x = StartUV.x;
CurUV.y += PixelSize.y;
}
// 가중치 총합 256 으로 정규화
// 이렇게 해야 모든 샘플 값이 커널 가중치만큼 더해져서 전체 이미지가 지나치게 밝아지거나 어두워지는 것 방지
ResultColor /= 65536.0f;
return ResultColor;
}
9x9까지 해버리면 연산량이 너무 많아집니다. 이에 대한 해결책으로 [원본 이미지 다운 스케일링 -> 5x5정도의 필터 적용 -> 적용된 이미지 원래 크기로 업스케일링]을 실시할 수도 있습니다(효과는 동일한데 연산은 최적화됨). 여기서는 적용에 의의를 두는 것이기 때문에 5x5, 7x7, 9x9를 각각 실시해봤습니다.
2. OldFilm
: 특수한 텍스쳐를 사용했습니다.

예전에 Cuphead라는 게임을 모작할 때 사용했던 리소스인데, 똑같이 가져와서 적용해봤습니다.
#include "PrecompileHeader.h"
#include "Ext_OldFilm.h"
#include <DirectX11_Base/Base_Windows.h>
#include "Ext_MeshComponentUnit.h"
void Ext_OldFilm::Start()
{
// 기본 텍스쳐들 로드
{
Base_Directory Dir;
Dir.MakePath("../Resource/FX/ScreenFX");
std::vector<std::string> Paths = Dir.GetAllFile({ "png" });
for (const std::string& FilePath : Paths)
{
Dir.SetPath(FilePath.c_str());
std::string ExtensionName = Dir.GetExtension();
std::string FileName = Dir.GetFileName();
Textures.push_back(Ext_DirectXTexture::LoadTexture(FilePath.c_str()));
}
}
OFData.OldFilmValue.x = 3.f;
MaxIndex = static_cast<int>(Textures.size()) - 1;
PostUnit = std::make_shared<Ext_MeshComponentUnit>();
PostUnit->MeshComponentUnitInitialize("FullRect", "OldFilm");
PostTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL);
}
void Ext_OldFilm::PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
AccTime += _DeltaTime;
if (AccTime >= 0.1f)
{
AccTime = 0.0f;
++CurIndex;
if (CurIndex > MaxIndex)
{
CurIndex = 0;
}
}
PostUnit->GetBufferSetter().SetTexture(Textures[CurIndex], "DiffuseTex");
PostUnit->GetBufferSetter().SetConstantBufferLink("OldFilmData", OFData);
PostTarget->RenderTargetClear();
PostTarget->RenderTargetSetting();
PostUnit->Rendering(_DeltaTime);
PostUnit->GetBufferSetter().AllTextureResourceReset();
_MainRenderTarget->RenderTargetSetting();
_MainRenderTarget->Merge(PostTarget);
}
cbuffer OldFilmData : register(b0)
{
float4 OldFilmValue; // x 성분만 사용
}
Texture2D DiffuseTex : register(t0);
SamplerState Sampler : register(s0);
struct PSInput
{
float4 Position : SV_POSITION;
float2 Texcoord : TEXCOORD0;
};
float4 OldFilm_PS(PSInput IN) : SV_TARGET
{
// 1) 스크린 얼룩(Film) 텍스처 샘플
float4 FlimColor = DiffuseTex.Sample(Sampler, IN.Texcoord);
// 2) 얼룩 마스크 강도 계산 (1?R) * OldFilmValue.x
float Mask = (1.0f - FlimColor.r) * OldFilmValue.x;
// 3) R, G, B, A 모두 동일한 마스크 값으로
return float4(Mask, Mask, Mask, Mask);
}
3. Distortion
: 가장 흔하게 사용되는 왜곡 효과입니다. 보통 특정 부분만 마스킹해서 사용하는데, 여기서는 그냥 화면 대상 모든 픽셀에 적용해봤습니다.
#include "PrecompileHeader.h"
#include "Ext_Distortion.h"
#include <DirectX11_Base/Base_Windows.h>
#include "Ext_MeshComponentUnit.h"
void Ext_Distortion::Start()
{
PostUnit = std::make_shared<Ext_MeshComponentUnit>();
PostUnit->MeshComponentUnitInitialize("FullRect", "Distortion");
PostTarget = Ext_DirectXRenderTarget::CreateRenderTarget(DXGI_FORMAT::DXGI_FORMAT_R32G32B32A32_FLOAT, Base_Windows::GetScreenSize(), float4::ZERONULL);
}
void Ext_Distortion::PostProcessing(Ext_DirectXRenderTarget* _MainRenderTarget, std::shared_ptr<class Ext_Camera> _Camera, float _DeltaTime)
{
PostTarget->RenderTargetClear();
PostTarget->RenderTargetSetting();
FData.ScreenSize = Base_Windows::GetScreenSize();
FData.AccTime += _DeltaTime;
PostUnit->GetBufferSetter().SetConstantBufferLink("FrameData", FData);
PostUnit->GetBufferSetter().SetTexture(_MainRenderTarget->GetTexture(0), "DiffuseTex");
PostUnit->Rendering(_DeltaTime);
PostUnit->GetBufferSetter().AllTextureResourceReset();
_MainRenderTarget->RenderTargetClear();
_MainRenderTarget->Merge(PostTarget);
}
cbuffer FrameData : register(b0)
{
float4 ScreenSize; // 화면 크기
float AccTime; // 실행된 시간
};
Texture2D DiffuseTex : register(t0);
SamplerState Sampler : register(s0);
struct PSInput
{
float4 Position : SV_POSITION; // 스크린 위치 (필요 없으면 생략 가능)
float2 Texcoord : TEXCOORD0; // 0~1 범위 UV
};
float4 Distortion_PS(PSInput _Input) : SV_TARGET
{
float2 UV = _Input.Texcoord;
// 파라미터, 상수버퍼로 전달하면 조절 가능
const float Frequency = 10.0f; // 파동 수
const float Ample = 20.0f; // 진폭 분모
// 공식
UV.x += sin(UV.y * Frequency + AccTime) / Ample;
return DiffuseTex.Sample(Sampler, UV);
}
'DirectX11 > 프레임워크 제작' 카테고리의 다른 글
| [DirectX11] 스카이박스 만들어보기 (0) | 2025.06.21 |
|---|---|
| [DirectX11] 반투명 물체 테스트하기 (0) | 2025.06.21 |
| [DirectX11] 그림자 매핑 해보기 (0) | 2025.06.12 |
| [DirectX11] 디퍼드 렌더링으로 변경하기 (0) | 2025.06.11 |
| [DirectX11] 노말 매핑 해보기 (0) | 2025.06.07 |