티스토리 뷰
이번 시간에는 DirectX12를 활용한 Bloom 효과에 대해 알아보겠습니다.
Bloom은 굉장히 밝은 물체를 봤을 때 생기는 현상을 모니터로 표현하는 것입니다.
PostProcess를 통해 구현할 수 있으며 간단한 연산으로 구현할 수 있기에 적은 비용으로 이미지에 사실감을 더해줄 수 있습니다.
가장 먼저 Bloom 효과를 실제로 구현한 모습부터 보겠습니다.

밝게 빛나는 부분들 근처가 뽀얗게 보이는 효과가 바로 Bloom 입니다.
Bloom 효과를 구현하는 과정을 이미지로 요약하면 아래와 같습니다.

요약
Rendering Pass는 크게 3 단계로 나뉩니다.
첫 번째 Pass는 Render Original Scene Pass로 구체들을 기본적인 Phong Shading으로 렌더링하는 것입니다.
다만 단순히 씬을 그릴 뿐만 아니라 밝은 부분만 따로 추려서 BrightArea Texture에 렌더링해주어야 합니다.
별도 Pass를 추가해서 그릴 수도 있기는 하지만 효율을 위해 Multi Render Targets을 통해서 한 번의 Pass로 그렸습니다.
두 번째 Pass는 BrightArea Texture에 Blur 효과를 주는 것입니다.
Blur는 효과가 잘 나타날 수 있도록 여러 번 진행했습니다.
세 번째 Pass는 원래의 이미지와 Blur된 이미지를 합쳐서 Bloom 효과가 나는 이미지를 만드는 Pass입니다.
밝게 표시된 영역만 Blur를 해서 원래 이미지에 합쳤기 때문에 밝은 부분 근처에 뿌옇게 빛이 보이는 Bloom 효과가 구현되었습니다.
구현 내용
이제 본격적으로 각 Pass를 어떻게 구현하였는지 코드와 함께 살펴보겠습니다.
1. Render Original Scene Pass
ComPtr<ID3D12Resource> m_BrightAreaRenderTarget;
가장 먼저 해주어야 할 일은 RenderTarget으로 쓰일 Resource를 만들어 주는 것입니다.
ID3D12Resource 타입으로 만들어 주었고, 밝은 영역만 담을 예정이므로, m_BrightAreaRenderTarget이라는 이름을 붙여주었습니다.
D3D12_HEAP_PROPERTIES heapProperties;
heapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
heapProperties.Type = D3D12_HEAP_TYPE_DEFAULT;
heapProperties.CreationNodeMask = 1;
heapProperties.VisibleNodeMask = 1;
D3D12_RESOURCE_DESC renderTargetDesc = {};
renderTargetDesc.Width = GetClientWidth();
renderTargetDesc.Height = GetClientHeight();
renderTargetDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
renderTargetDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
renderTargetDesc.MipLevels = 1;
renderTargetDesc.DepthOrArraySize = 1;
renderTargetDesc.SampleDesc.Count = 1;
renderTargetDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
device->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&renderTargetDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
nullptr,
IID_PPV_ARGS(&m_BrightAreaRenderTarget)
);
CreateCommittedResource 함수를 통해 리소스를 만들었습니다.
리소스를 만든 다음에 할 일은 Descriptor를 만드는 것입니다.
RenderTarget 용도로 쓰일 것이므로 RenderTargetView를 만들어야 합니다.
D3D12_RENDER_TARGET_VIEW_DESC rtvDesc = {};
rtvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
rtvDesc.Texture2D.MipSlice = 0;
rtvDesc.Texture2D.PlaneSlice = 0;
UINT rtvIncrementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle2 = m_RTV_Heap->GetCPUDescriptorHandleForHeapStart();
rtvHandle2.ptr += 1 * rtvIncrementSize;
device->CreateRenderTargetView(m_BrightAreaRenderTarget.Get(), &rtvDesc, rtvHandle2);
CreateRenderTargetView 함수를 통해 Descriptor를 만들었습니다.
RenderTargetView를 만들 때 m_RTV_Heap이라는 RenderTargetView들을 담아두는 Heap에 만들었습니다.
원래의 씬을 그리는 RenderTargetView를 m_RTV_Heap의 시작 위치에 만들었으므로, 새로 만들어진 RenderTargetView는 rvtIncrementSize만큼 위치를 이동시켜 그 다음 위치에 만들었습니다.
그 다음으로 할 일을 Render Pipeline의 결과를 해당 RenderTargets에 그리겠다고 명시하는 것입니다.
D3D12_CPU_DESCRIPTOR_HANDLE rtvCpuHandle[2];
rtvCpuHandle[0] = m_RTV_Heap->GetCPUDescriptorHandleForHeapStart();
rtvCpuHandle[1] = m_RTV_Heap->GetCPUDescriptorHandleForHeapStart();
rtvCpuHandle[1].ptr += device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
commandList->OMSetRenderTargets(2, rtvCpuHandle, FALSE, &dsv);
OMSetRenderTargets 함수를 호출해서 명시해주었습니다.
마지막으로 할 일은 Pixel Shader을 수정하는 일입니다.
struct PS_OUTPUT
{
float4 color0 : SV_Target0;
float4 color1 : SV_Target1;
};
PS_OUTPUT main(PS_INPUT IN) : SV_TARGET
{
PS_OUTPUT o;
... 라이팅 계산 ...
o.color0 = 계산된 값;
float3 grayScaleColor = float3(0.333f, 0.333f, 0.333f);
float brightness = saturate(dot(o.color0, grayScaleColor));
float brightThresholdMin = 0.2f;
float brightThresholdMax = 0.8f;
float brightThresholdScale = (1.0f / (brightThresholdMax - brightThresholdMin));
float brightnessWeight = saturate(saturate(brightness - brightThresholdMin) * brightThresholdScale);
o.color1 = float4(brightnessWeight * SumColor, 1.0f);
return o;
}
PS_OUTPUT이라는 구조체를 정의하고 Pixel Shader 함수의 리턴 값을 PS_OUTPUT 구조체로 변경했습니다.
color0에는 원래의 씬을 렌더링한 결과가 들어갈 것이므로 라이팅을 계산한 값을 전달했습니다.
color1에는 밝은 부분은 원래 색을 그대로 전달하고, 어두운 부분은 0이 되도록 계산한 값을 전달했습니다.
밝은 부분은 다양한 방식을 통해서 계산할 수 있습니다.
예제에서는 R, G, B 채널에 각각 0.3333을 곱한 뒤 더한 결과 값에 따라 weight를 계산해서 원래의 색에 곱했습니다.
결과 값이 최소 값(0.2) 이하이면 0이 되고, 최대 값(0.8) 이상일 때 1이 되도록 선형 보간 방식으로 weight 값을 계산했습니다.
2. Blur Pass
다음으로 할 일은 Bright Area Texture에 Blur 처리를 하는 것입니다.
단순히 한 번의 Pass를 돌려서 Blur를 할 수도 있긴 하지만, 이번 예제에서는 조금 독특한 방법을 사용했습니다.
Blur를 할 때 상하 좌우 주변의 픽셀을 샘플링하는 대신에, 가로, 세로 픽셀들을 샘플링하는 것을 번갈아 가면서 진행했습니다.
간단한 이미지를 통해 Sampling하는 모습을 표현하면 아래와 같습니다.

왼쪽 이미지는 중간에 한 픽셀을 처리 하기 위해 샘플링한 픽셀들을 표현하는 이미지입니다.
5x5 필터를 사용하게 되면 한 번의 Pass에서 총 25개의 픽셀을 샘플링하게 됩니다.
중앙에 있는 이미지는 5 크기의 필터를 이용해 가로로 블러처리한 것을 표현한 이미지입니다.
한 픽셀을 처리하기 위해 총 5번의 샘플링을 했습니다.
하지만 여기서 끝낼 경우 세로로 있는 픽셀들을 표현할 수 없으므로 5 크기의 필터를 이용해 세로로 블러처리를 하는 Pass를 추가합니다.
이렇게 하게 되면 총 두 번의 Pass와 총 10번 샘플링으로 비슷한 효과를 내게 됩니다.
또한 가로 한 번, 세로 한 번으로 Blur를 끝내도 되지만 Blur 결과를 다시 Input으로 주고 위와 같은 과정을 반복하면 좀 더 자연스러운 Blur 효과를 낼 수 있기에 여러 번 반복해서 Blur를 하였습니다.
위와 같은 방식의 Blur를 구현하려면 Blur 중간 내용을 담아둘 Texture가 필요합니다.
ComPtr<ID3D12Resource> m_BlurTempRenderTarget;
m_BlurTempRenderTarget이라는 이름으로 ID3D12Resource를 선언하였고, 기존 다른 Texture들처럼 CreatedCommitedResource 함수를 통해 리소스를 생성했습니다. (코드 생략)
이 텍스쳐는 처음에는 RenderTarget으로 쓰이지만 다음 Blur Pass에서는 Shader의 Input으로 사용될 예정입니다.
따라서 RenderTargetView 뿐만 아니라 ShaderResourceView도 만들어서 연결해주어야 합니다. (코드 생략)
기존에 밝을 부분을 추려서 저장하는 RenderTarget 용도로 만들었던 m_BrightAreaRenderTarget도 Shader의 Input으로 쓰일 수 있으므로 ShaderResourceView를 만들어서 연결해주어야 합니다. (코드 생략)
m_BrightAreaRenderTarget 및 m_BlurTempRenderTarget 텍스쳐를 이용해서 Blur Pass를 진행하는 과정을 도식화하면 아래와 같습니다.

가장 먼저 m_BrightAreaRenderTarget을 입력으로 하여 Horizontal Blur Pass를 돌려서 결과를 m_BlurTempRenderTarget에 저장합니다.
그 후 m_BlurTempRenderTarget을 입력으로 하여 Vertical Blur Pass를 돌려서 결과를 m_BrightAreaRenderTarget에 저장합니다.
위와 같은 과정을 적당한 횟수만큼 반복하면 Blur가 된 이미지를 얻을 수 있게 됩니다.
아래 코드는 실제로 위와 같은 흐름을 구현한 코드입니다.
D3D12_RESOURCE_BARRIER barriers[2] = {};
barriers[0].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barriers[0].Transition.pResource = m_BlurTempRenderTarget.Get();
barriers[0].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
barriers[1].Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
barriers[1].Transition.pResource = m_BrightAreaRenderTarget.Get();
barriers[1].Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
int blurNum = 5;
UINT rtvIncrementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
UINT dsvIncrementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
UINT srvIncrementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
UINT samplerIncrementSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER);
for (int i = 0; i < blurNum * 2; i++)
{
D3D12_CPU_DESCRIPTOR_HANDLE rtvCpuHandle;
rtvCpuHandle = m_RTV_Heap->GetCPUDescriptorHandleForHeapStart();
rtvCpuHandle.ptr += ((i + 1) % 2 + 1) * rtvIncrementSize;
D3D12_CPU_DESCRIPTOR_HANDLE dsv = m_DSV_Heap->GetCPUDescriptorHandleForHeapStart();
dsv.ptr += dsvIncrementSize;
commandList->OMSetRenderTargets(1, &rtvCpuHandle, FALSE, &dsv);
FLOAT clearColor[] = { 0.0f, 0.0f, 0.0f, 1.0f };
commandList->ClearRenderTargetView(rtvCpuHandle, clearColor, 0, nullptr);
commandList->ClearDepthStencilView(dsv, D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
commandList->RSSetViewports(1, &m_Viewport);
commandList->RSSetScissorRects(1, &m_ScissorRect);
commandList->SetPipelineState(m_BlurPipelineState.Get());
commandList->SetGraphicsRootSignature(m_BlurRootSignature.Get());
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &m_QuadVertexBufferView);
commandList->IASetIndexBuffer(&m_QuadIndexBufferView);
ID3D12DescriptorHeap* ppHeaps[] = { m_Blur_SRV_CBV_Heap.Get(), m_BlurSampler_Heap.Get() };
commandList->SetDescriptorHeaps(_countof(ppHeaps), ppHeaps);
D3D12_GPU_DESCRIPTOR_HANDLE srvHeapStart = m_Blur_SRV_CBV_Heap->GetGPUDescriptorHandleForHeapStart();
srvHeapStart.ptr += (i % 2) * srvIncrementSize;
commandList->SetGraphicsRootDescriptorTable(0, srvHeapStart);
D3D12_GPU_DESCRIPTOR_HANDLE samplerHeapStart = m_BlurSampler_Heap->GetGPUDescriptorHandleForHeapStart();
samplerHeapStart.ptr += (i % 2) * samplerIncrementSize;
commandList->SetGraphicsRootDescriptorTable(1, samplerHeapStart);
m_BlurRootConstantData.horizontal = (float)(i % 2);
commandList->SetGraphicsRoot32BitConstants(2, 8, &m_BlurRootConstantData, 0);
commandList->DrawIndexedInstanced(m_QuadModel->IndicesCount(), 1, 0, 0, 0);
barriers[i % 2].Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
barriers[i % 2].Transition.StateAfter = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
barriers[(i + 1) % 2].Transition.StateBefore = D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE;
barriers[(i + 1) % 2].Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
commandList->ResourceBarrier(2, barriers);
}
DirectX에서는 Resource를 사용할 때 Resource Barrier를 통해 사용 용도를 전환해주어야 합니다.
Shader의 Input으로 사용될 것이라면 D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE로 두면 되고, Output으로 사용될 것이라면 D3D12_RESOURCE_STATE_RENDER_TARGET으로 두면 됩니다.
위 코드에서는 m_BrightAreaRenderTarget, m_BlurTempRenderTarget이 한 번의 Pass에서 각자 반대의 역할을 하게 되므로 나머지 연산을 통해 알맞게 셋팅될 수 있도록 했습니다.
RenderTargetView, ShaderResourceView, Sampler는 Blur Pass 내내 같은 Heap을 그대로 사용하지만, for문 내에서 iteration 마다 가리키는 위치를 다르게 주어 shader 관점에서는 input용으로 쓰이는 texture, sampler 하나만 사용해서 처리할 수 있도록 했습니다.
다음으로 현재 Blur Pass가 Horizontal일지, Vertical일지 결정하는 변수는 RootConstants를 통해서 Shader로 넘겼습니다.
m_BlurRootConstantData에 필요한 정보를 셋팅하고 SetGraphicsRoot32BitConstants 함수를 호출해 Shader로 넘겨서 매번 원하는 방식대로 동작할 수 있도록 했습니다.
마지막으로 할 일은 실제 Blur를 하는 Pixel Shader를 구현하는 것입니다.
Pixel Shader에서 구현한 내용은 아래와 같습니다.
cbuffer BlurRootConstant : register(b0)
{
float4 texelSize;
float isHorizontal;
};
float3 Blur(float2 uv)
{
// Horizontal Pass에서 isHorizontal 값은 1, isVertical 값은 자동으로 0이됨
// Vertical Pass에서 isHorizontal 값은 0, isVertical 값은 자동으로 1이됨
float isVertical = saturate(abs(isHorizontal - 1.0f));
// 0번은 중간 픽셀의 Weight로 사용될 예정이며, 나머지 9개는 좌우, 혹은 상하로 쓰일 예정
// 결론적으로 총 1 + 9 + 9 = 19개의 픽셀을 사용할 것이므로 균등하게 나눠서 Weight 값은 1 / 19 = 0.05263
float in_weights[10] = { 0.05263f, 0.05263f, 0.05263f, 0.05263f, 0.05263f, 0.05263f, 0.05263f, 0.05263f, 0.05263f, 0.05263f };
float3 result = gTexture.Sample(gSampler, uv).rgb * in_weights[0];
for(int i = 1; i < 10; ++i)
{
result += gTexture.Sample(gSampler, uv + float2(i * texelSize.z, 0.0f)).rgb * in_weights[i] * isHorizontal;
result += gTexture.Sample(gSampler, uv - float2(i * texelSize.z, 0.0f)).rgb * in_weights[i] * isHorizontal;
result += gTexture.Sample(gSampler, uv + float2(0.0f, i * texelSize.w)).rgb * in_weights[i] * isVertical;
result += gTexture.Sample(gSampler, uv - float2(0.0f, i * texelSize.w)).rgb * in_weights[i] * isVertical;
}
return result;
}
isHorizontal 값과 texelSize 값은 RootConstant를 통해 전달됩니다.
Blur를 할 때는 모든 픽셀에 균일하게 weight를 주는 방식을 사용했습니다.
Vertical 한 번, Horizontal 한 번 Blur를 한 결과는 아래와 같습니다.

왼쪽 이미지는 원본 이미지이고, 중앙 이미지는 Vertical Blur를 한 이미지이고, 오른쪽 이미지는 Horizontal Blur까지 한 이미지입니다.
Vertical, Horizontal을 번갈아 가면서 여러 차례 Blur를 하고나면 아래와 같은 이미지가 나오게 됩니다.

3. Add Pass
마지막 Add Pass는 아주 단순하게 구현했습니다.
앞서 Render Original Scene Pass에서 만들어진 Original Scene Texture와 Blur가 된 Bright Area Texture를 Shader의 Input으로 두고, 단순히 RGB 값을 더하는 Pass를 한 번 돌려서 구현했습니다.
단순히 RGB를 채널 별로 더했기 때문에 결과 값이 1을 넘는 경우가 발생할 수 있습니다.
1을 넘는 부분들은 다 1로 clamp되기 때문에 1을 넘는 픽셀이 많아지면 이미지가 부자연스럽게 느껴질 수 있습니다.
이 경우 HDR(High Dynamic Range) Rendering과 Tone Mapping을 이용하면 자연스러운 이미지를 모니터에 띄울 수 있습니다.
다만, 해당 내용까지 구현하면 너무 내용이 많아지므로 이번 내용은 여기서 마무리 짓도록 하겠습니다.
지금까지 DirectX12를 이용해서 Bloom 효과를 구현하는 방법에 대해 알아보았습니다.
Bloom 효과는 이해하기도 어렵지 않고 구현 난이도도 많이 높지 않기 때문에, DirectX12의 동작 방식을 이해하기 위해 연습용으로 구현해보면 좋을 것 같습니다.
참고 자료
https://learnopengl.com/Advanced-Lighting/Bloom
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/bloom-in-unreal-engine
'프로그래밍 > DirectX12' 카테고리의 다른 글
| DirectX12로 Compute Shader 사용하기 (0) | 2025.08.12 |
|---|---|
| DirectX12를 활용한 Deferred Shading (0) | 2025.07.22 |
| DirectX12를 활용한 Instanced Drawing (0) | 2025.03.03 |
| DirectX12에서의 CPU & GPU 동기화 (1) | 2025.01.26 |
| Microsoft PIX를 활용한 DirectX12 프로그램 디버깅 (0) | 2025.01.13 |
- Total
- Today
- Yesterday
- 중복 순열
- 조합
- Scriptable Render Pipeline
- C#
- Unreal
- 최적화
- perspective projection
- RubiksCube
- 루빅스큐브
- 순열
- collision detection
- MeshProcessing
- Mesh Processing
- normalized device coordinate
- RL
- opengl
- 중복 조합
- 유니티
- VTK
- Mesh
- AABB
- 값 형식
- DirectX12
- 수학
- Unity
- 통계학
- 경우의 수
- value type
- CollisionDetection
- 참조 형식
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |