Bloom을 구현하려면 우선
sceneHDR pass -> LDR ToneMapping pass
위 과정이 분리 구현 되어있어야한다.
이번 글은 해당 내용에 대한 이해가 있다는 것을 바탕으로 한다.
Mip (mipmap)
Bloom을 구현하기 전에 먼저 Mip의 개념에 대해 복습을 하고 가자. Mip(mipmap)은 같은 텍스처를 여러 해상도로 미리 만들어둔 배열이다. 주로 멀리있는 오브젝트에 텍스처를 샘플링할때 낮은 해상도의 이미지를 사용하여 최적화(앨리어싱 방지 등)를 하는 용도로 사용한다.
mip 0 : 1024 x 1024 (원본)
mip 1 : 512 x 512
mip 2 : 256 x 256
mip 3 : 128 x 128
mip 4 : 64 x 64
...
Bloom에서는 이 Mip의 개념을 조금 다르게 사용한다.
Bloom

Bloom은 밝은 부분을 추출하여 퍼뜨려 빛이 번지는 느낌을 주는 기법이다. 쉽게 생각하면 밝은 픽셀들을 추출하고 블러처리하여 원본에 더해준다고 생각하면 된다. Bloom 처리를 위해서는 HDR 컬러(SceneHDR)를 기반으로 3개의 패스를 추가로 수행한다.
1. Prefilter : Bloom 처리할 픽셀만 골라낸다
2. Downsample+Blur : 여러 해상도로 줄이면서 퍼짐 반경을 만든다
3. Upsample+Combine : 그 반경들을 다시 합쳐서 BloomFinal 하나로 만든다
이렇게 생성한 BloomFinal 텍스처(mip0) 를 PostProcess 단계에서 SceneHDR에 더해주면 밝은 부분이 퍼지는 느낌을 줄 수 있다.
1. Bloom Prefilter Pass
sceneHDR 텍스처에서 Bloom처리할 밝은 부분을 추출한다.
Input : sceneHDR
...SceneHDR에서 밝은 부분 추출
Output : BloomA mip0
| 기능 | 설명 |
| Luminance | float3 RGB 색을 float 밝기값으로 줄인 것. Bloom에서 픽셀의 밝기를 판단하는 기준으로 사용 |
| Threshold | Bloom 처리 임계값. luminance > threshold 인 경우 Bloom에 기여 |
| Soft Knee | 임계값 근처를 부드럽게 처리하여 경계가 뚝 끊어지지 않게 함 |
| Clamp | 너무 밝은 값 제한(아티팩트 방지) |
| Downsample | mip0을 원본 해상도가 아닌, 처음부터 half-res로 뽑기(권장) |
2. Bloom Downsample + Blur Pass
Bloom 텍스처에 해상도를 줄이며(Downsample) 여러 mip을 만들고, 블러 처리한다. 이때 하나의 텍스처를 동시에 read/write 할 수 없기 때문에 BloomA, BloomB 두 텍스처를 번갈아 사용하는 ping-pong 방식이 필요하다.
- Read : BloomA mip(i-1)
- Write : BloomB mip(i)
- 다음 단계에서 A/B를 스왑(또는 반대로 수행)
Bloom에서는 mip을 ‘퍼짐 반경이 다른 블러 결과’를 저장하는 용도로 사용한다.
- mip0(가장 큰 해상도) : 선명하고 작은 퍼짐(코어)
- mipN(가장 작은 해상도) : 넓고 흐린 퍼짐(헤일로)
1번 패스에서 mip0을 생성했기 때문에 mip1 ~ MipCount-1 레벨까지의 mip을 생성해주면 된다. ping-pong 방식을 이용하기 때문에 한 SRV(texture)에 모든 mip이 기록되지 않는다.
Input : Bloom mip(i-1)
... mip(i-1)을 다운샘플링+Blur 처리하여 기록
Output : Bloom mip(i)
| 기능 | 설명 |
| Downsample | mip(i-1) → mip(i)로 해상도를 줄이며 여러 mip을 생성 |
| Blur | 다운샘플하면서 동시에 블러를 적용하여 색상을 퍼뜨림 |
| Blur Kernel / Convolution | 주변 픽셀을 얼마나, 어떤 비율로 섞을지 정의한 가중치 집합 (Gaussian, Kawase) |
| Separable Blur | 블러 비용을 줄이기 위한 구현 방식(수평/수직 분리 등) |
| Mipmap 체인 생성 | 자동 mip 생성이 아니라, 렌더 패스로 각 mip을 수동으로 채움 |
3. Bloom Upsample + Combine Pass
mipN(가장 작은 해상도, 넓은 퍼짐)부터 mip0(큰 해상도, 날카로운 코어)로 올라오며 업샘플하고 가산 합성하여 최종 Bloom 텍스처를 만든다. 결과적으로 ‘넓은 퍼짐 + 중간 퍼짐 + 날카로운 코어’가 전부 합쳐진 최종 Bloom 텍스처를 생성하게 된다.
다운샘플시에 ping-pong 방식을 이용했기 때문에 lastMip이 BloomA에 있는지, BloomB에 있는지 추적하여 업샘플을 시작해야한다. 또 같은 이유로 mip(i)와 mip(i+1)은 한 텍스처에 존재하지 않는다. 둘을 더하기 위해서는 BloomA와 BloomB 모두 SRV 바인딩이 필요한데, 하나의 텍스처를 동시에 read, write로 바인딩 할 수 없기 때문에 따로 가산 합성용 텍스처를 추가로 사용해야한다.
즉, downsample 결과(BloomA/B에 분산된 mip들)를 읽기 전용으로 두고, 누적 결과는 따로 생성한 AccumA/B에 쌓아가며 최종 Accum.mip0을 생성한다.
Input : mip(i), mip(i+1)
... mip(i+1)을 업샘플링하여 mip(i)과 가산 -> 가산합성용 텍스처에 기록
Output : BloomFinal
| 기능 | 설명 |
| Upsample | 작은 해상도의 mip을 더 큰 mip으로 확대 (mipN → mip(N-1) → … → mip0) |
| Combine | 업샘플한 결과를 다음 큰 mip에 더해 누적 (mip(i) += upsample(mip(i+1))) |
| Additive Composition | Bloom은 빛이므로 보통 add(가산 합성) |
| Scatter / Diffusion | 업샘플 시 가중치를 조절해서 퍼짐 강도(룩)를 조절 |
| Lens Dirt | 최종 Bloom에 dirt 텍스처를 곱하거나 가산하여 먼지/렌즈 느낌을 추가 |
DirectX11 Bloom 구현하기
// Render Pass
SceneHDRRender(); // Gaometry + Lighting + Shadow
BloomProcess(); // Bloom
PostProcess(); // ToneMapping + PostProcess + ScreenFx
Bloom의 처리는 기존의 sceneHDR를 생성한 이후, PostProcess 이전 단계에 진행하면 된다. sceneHDR을 소스로 Bloom 처리한 Texture를 생성하고, PostProcess단계에서 해당 리소스를 더해주면 된다.
Bloom의 구현은 크게 3단계로 이루어진다.
- 밝은 부분을 추출하는 Prefilter Pass
- Mip Chain을 형성하고 블러처리하는 DownSample Blur Pass
- Mip들을 가산합산하며 최종 블룸 이미지를 만드는 Upsample Combine Pass
이중 2, 3 단계는 Mip의 개수만큼 처리해야하기 때문에 mipCount-1 회 만큼 반복해서 드로우콜을 호출해준다.

파라미터와 상수버퍼는 유니티의 Bloom 프로퍼티를 참고하여 작성하였다.
struct alignas(16) BloomCB
{
float bloom_threshold = 1.0f;
float bloom_intensity = 0.5f;
float bloom_scatter = 0.5f;
float bloom_clamp = 0.0f;
Vector3 bloom_tint = { 1.0f, 1.0f, 1.0f };
int padding;
int srcMip = 0; // SampleLevel용 mip 인덱스
Vector2 srcTexelSize = { 0.0f, 0.0f }; // 패스에서 읽고 있는 mip 레벨의 텍스처 해상도를 기준으로 한 texel size
int padding2;
};
Bloom을 구현하면서 가장 골치 아팠던 것이 SRV와 RTV의 충돌이었는데, 나는 그냥 안전하게 가산 합성용 D3D 리소스를 따로 만들어서 구현하였다. RTV를 mip별로 만들어 input과 output의 해상도를 달리 연결하면 알아서 다운샘플, 업샘플링이 된다.
static ComPtr<ID3D11Texture2D> bloomATexture;
static ComPtr<ID3D11Texture2D> bloomBTexture;
static ComPtr<ID3D11ShaderResourceView> bloomASRV;
static ComPtr<ID3D11ShaderResourceView> bloomBSRV;
static std::vector<ComPtr<ID3D11RenderTargetView>> bloomARTVs;
static std::vector<ComPtr<ID3D11RenderTargetView>> bloomBRTVs;
static ComPtr<ID3D11Texture2D> accumATexture;
static ComPtr<ID3D11Texture2D> accumBTexture;
static ComPtr<ID3D11ShaderResourceView> accumASRV;
static ComPtr<ID3D11ShaderResourceView> accumBSRV;
static std::vector<ComPtr<ID3D11RenderTargetView>> accumARTVs;
static std::vector<ComPtr<ID3D11RenderTargetView>> accumBRTVs;
쉐이더는 GPT를 활용하여 간단하게 작성하였다.
/*
[ Bloom Prefilter Pixel Shader ]
Bloom 처리 1단계.
sceneHDR 텍스처에서 Bloom처리할 밝은 부분을 추출한다.
- Input : sceneHDR
- Output : BloomA mip0
*/
#include <shared.fxh>
Texture2D sceneHDR : register(t12);
SamplerState samLinearClamp : register(s2);
float Luminance(float3 c)
{
return dot(c, float3(0.2126, 0.7152, 0.0722));
}
// soft-knee
float PrefilterMask(float lum, float threshold, float knee01)
{
float knee = max(1e-4, knee01) * threshold;
float x = lum - (threshold - knee);
float y = saturate(x / (2.0 * knee));
float soft = y * y * (3.0 - 2.0 * y);
return soft * step(threshold - knee, lum);
}
float4 main(PS_FullScreen_Input input) : SV_TARGET
{
float3 hdr = sceneHDR.Sample(samLinearClamp, input.uv).rgb;
// 너무 밝은값 clamp
if (bloom_clamp > 0.0f)
hdr = min(hdr, bloom_clamp.xxx);
float lum = Luminance(hdr);
float m = PrefilterMask(lum, bloom_threshold, bloom_scatter);
// tint
float3 outcolor = hdr * m * bloom_tint;
return float4(outcolor, 1);
}
/*
[ Bloom Downsample Blur Pixel Shader ]
Bloom 처리 2단계.
해상도를 줄이며(Downsample) 여러 mip을 만들고, 블러 처리한다.
하나의 텍스처를 동시에 read/write 할 수 없기 때문에 BloomA, BloomB 두 텍스처를
번갈아 사용하는 ping-pong 방식을 사용한다.
- mip0(가장 큰 해상도) : 선명하고 작은 퍼짐
- mipN(가장 작은 해상도) : 넓고 흐린 퍼짐
- Input : Bloom mip(i-1)
- Output : Bloom mip(i)
*/
#include <shared.fxh>
Texture2D bloomSrc : register(t13); // C++에서 A 또는 B를 바인딩 (ping-pong)
SamplerState samLinearClamp : register(s2);
float3 SampleSrc(float2 uv)
{
return bloomSrc.SampleLevel(samLinearClamp, uv, srcMip).rgb;
}
float4 main(PS_FullScreen_Input input) : SV_TARGET
{
float2 uv = input.uv;
float2 t = srcTexelSize;
// 가벼운 9-tap (근사 가우시안)
float3 c = 0;
c += SampleSrc(uv + t * float2(-1, -1));
c += SampleSrc(uv + t * float2(0, -1)) * 2.0;
c += SampleSrc(uv + t * float2(1, -1));
c += SampleSrc(uv + t * float2(-1, 0)) * 2.0;
c += SampleSrc(uv) * 4.0;
c += SampleSrc(uv + t * float2(1, 0)) * 2.0;
c += SampleSrc(uv + t * float2(-1, 1));
c += SampleSrc(uv + t * float2(0, 1)) * 2.0;
c += SampleSrc(uv + t * float2(1, 1));
c *= (1.0 / 16.0);
return float4(c, 1);
}
/*
[ Bloom Upsample Combine Pixel Shader ]
Bloom 처리 3단계.
mipN(가장 작은 해상도, 넓은 퍼짐)부터 mip0(큰 해상도, 날카로운 코어)로 올라오며
업샘플하고 가산 합성하여 최종 Bloom 텍스처를 만든다.
- Input : BloomA mipN
- Output : BloomFinal (BloomA mip0 또는 BloomB mip0)
*/
#include <shared.fxh>
Texture2D bloomBig : register(t13); // big 해상도 mip 체인 (base)
Texture2D bloomSmall : register(t14); // small 해상도 mip 체인 (업샘플 소스)
SamplerState samLinearClamp : register(s2);
float4 main(PS_FullScreen_Input input) : SV_TARGET
{
float2 uv = input.uv;
float3 big = bloomBig.SampleLevel(samLinearClamp, uv, srcMip).rgb;
float3 small = bloomSmall.SampleLevel(samLinearClamp, uv, srcMip + 1).rgb; // bilinear 업샘플
// scatter
float w = lerp(0.5, 1.0, saturate(bloom_scatter));
// combine (합산)
float3 combined = big + small * w;
return float4(combined, 1);
}
이렇게 생성한 최종 Bloom 이미지를 포스트 프로세싱 단계에서 더해주기만 하면 된다!
Texture2D bloomFinal : register(t13);
SamplerState samplerLinear : register(s0);
float4 main(PS_FullScreen_Input input) : SV_TARGET
{
... 생략
// Bloom
if (useBloom)
{
float3 bloom = bloomFinal.Sample(samplerLinear, uv).rgb;
hdr += bloom * bloom_intensity;
}
.. 생략
}
나름 만족스럽게 나오고 있다!
stage setting 코드는 아래 깃허브의 16_PostProcess 프로젝트를 참고하면 될 것 같다.
GitHub - wooj22/DirectX3D11_Study: DirectX3D11 공부용 레포지토리입니다.
DirectX3D11 공부용 레포지토리입니다. Contribute to wooj22/DirectX3D11_Study development by creating an account on GitHub.
github.com
'Programming > 컴퓨터그래픽스 (DX 11)' 카테고리의 다른 글
| [DirectX11] 디퍼드 렌더링 (Deferred Rendering) (0) | 2025.12.30 |
|---|---|
| [DirectX11] IBL (Image-Based Lighting) (0) | 2025.12.11 |
| [DirectX11] PBR Pixel Shader 구현하기 (0) | 2025.12.11 |
| [PBR] 4. BRDF (0) | 2025.12.05 |
| [PBR] 3. 프레넬 효과(Fresnel)와 미세면 모델(Microfacet model) (0) | 2025.12.04 |