[목차]

 

- 포스트 프로세싱이란

- 포스트 프로세싱 적용해보기

 

 

[포스트 프로세싱이란]

 

포스트 프로세싱은 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);
}

 

 

+ Recent posts