이번 포스팅은 URP CustomRenderFeature에서 Box블러와 Gaussian블러에 대해 분석과 구현 방법에 대해 작성하겠습니다.
https://github.com/CatDarkGame/CustomRenderFeatureExample/releases/tag/Blur
블러 기본 원리
우선 포토샵에서 특정 이미지의 투명도를 낮추고 8방향 조금씩 이동하며 복사하면 블러느낌을 낼 수 있습니다.
다양한 블러 알고리즘들이 있지만 오늘 소개할 Box블러와 Gaussian블러는 이런 원리로 흐림효과를 내는 블러 알고리즘입니다.
Box 블러
박스블러는 픽셀 하나하나 돌아다니면서 주변 픽셀의 값의 평균치를 계산하는 원리입니다.
P5 = (P1 + P2 + P3 + P4 + P5 + P6 + P7+ P8 + P9) / 9
일반적으로 평균값을 구하는 공식을 통해 픽셀의 평균값을 구할 수 있습니다.
위 예시에서는 주변 픽셀 1칸씩만 계산했지만, 2, 3칸 더 넓은 영역을 계산하면 블러 효과가 더 강해집니다.
Box블러 쉐이더
// 8방향 전부 계산하는 일반 BoxBlur
float4 BoxblurPassFragment (Varyings i) : SV_TARGET
{
float4 col = 0.0f;
int samples = (2 * _blurSamples) + 1;
for(float x=0; x<samples; x++)
{
for(float y=0; y<samples; y++)
{
float2 offset = float2(x - _blurSamples, y - _blurSamples);
col += tex2D(_MainTex, i.uv + (offset * _MainTex_TexelSize));
}
}
return float4(col.rgb / (samples * samples), 1);
}
쉐이더 코드는 위와 같습니다.
이전 포스팅에서 소개했던 CustomRenderPass을 이용해 쉐이더에 화면 텍스처 정보를 보내고
_MainTex_TexelSize변수를 이용해 주변 픽셀을 검출하는 맥락의 쉐이더입니다.
_MainTex_TexelSize란?
일명 텍셀값이라고 하며 픽셀의 XY좌표라고 이해하시면 됩니다.
추가로 주변 픽셀을 검사하는 영역을 유동적으로 조절할 수 있도록 _blurSamples라는 변수로 반복 & 샘플링 횟수를 제어 가능하게 했습니다.
RenderPass
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get(PASS_TAG);
// 임시렌더텍스처 생성
CameraData cameraData = renderingData.cameraData;
RenderTextureDescriptor descriptor = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
cmd.GetTemporaryRT(PROPERTY_TEMPBUFFER_1, descriptor, FilterMode.Bilinear);
cmd.SetGlobalFloat(PROPERTY_BLURSTEP, blurSamples);
// BoxBlur 렌더링
cmd.Blit(_destination, _tempBuffer_1, _material, 0);
// 임시렌더텍스처를 화면렌더텍스처에 복사
cmd.Blit(_tempBuffer_1, _destination);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
CustomRenderPass는 위와 같이 심플합니다.
결과물
샘플링 횟수 1~9까지 대략적인 차이를 확인할 수 있습니다.
Box 블러 - 2 Pass 방식
앞서 소개한 Box블러는 제대로된 블러효과를 볼려면 9샘플정도 올려야하는데
1개의 픽셀당 81번의 샘플링 & 계산을 해야하기 때문에 퍼포먼스가 많이 요구됩니다.
그래서 먼저 좌우로 블러 작업을 하고 다음 위 아래로 블러 작업을 하는 2 Pass 방식이 있습니다.
대각선 픽셀 검사를 안해도 되니 더욱 가벼운 퍼포먼스가 요구됩니다.
2Pass Box블러 쉐이더
// 가로세로만 계산하는 BoxBlur - 가로
float4 Boxblur_HorizontalPassFragment (Varyings i) : SV_TARGET
{
float4 col = 0.0f;
int samples = (2 * _blurSamples) + 1;
for(float x=0; x<samples; x++)
{
float2 offset = float2(x - _blurSamples, 0.0f);
col += tex2D(_MainTex, i.uv + (offset * _MainTex_TexelSize));
}
return float4(col.rgb / samples, 1);
}
// 가로세로만 계산하는 BoxBlur - 세로
float4 Boxblur_VerticalPassFragment (Varyings i) : SV_TARGET
{
float4 col = 0.0f;
int samples = (2 * _blurSamples) + 1;
for(float y=0; y<samples; y++)
{
float2 offset = float2(0.0f, y - _blurSamples);
col += tex2D(_MainTex, i.uv + (offset * _MainTex_TexelSize));
}
return float4(col.rgb / samples, 1);
}
쉐이더는 위와 같이 가로 & 세로 별도의 Pass로 작성됩니다.
RenderPass
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get(PASS_TAG);
// 임시렌더텍스처 생성
CameraData cameraData = renderingData.cameraData;
RenderTextureDescriptor descriptor = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
cmd.GetTemporaryRT(PROPERTY_TEMPBUFFER_1, descriptor, FilterMode.Bilinear);
cmd.GetTemporaryRT(PROPERTY_TEMPBUFFER_2, descriptor, FilterMode.Bilinear);
cmd.SetGlobalFloat(PROPERTY_BLURSTEP, blurSamples);
// 2 Pass BoxBlur 렌더링
cmd.Blit(_destination, _tempBuffer_1, _material, 1); // Horizontal
cmd.Blit(_tempBuffer_1, _tempBuffer_2, _material, 2); // Vertical
// 임시렌더텍스처를 화면렌더텍스처에 복사
cmd.Blit(_tempBuffer_2, _destination);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
CustomRenderPass에서는 이전 포스팅에서 소개한 2 Pass 방식으로 렌더링합니다.
결과물 & 퍼포먼스 비교
2개의 방식을 비교했을때
퀄리티는 거의 흡사하고, GPU 프로파일링을 해보니 2Pass방식이 약 6배 가볍게 나왔습니다. (마이크로초 us)
Gaussian 블러
가우시안블러는 박스블러와 블러 방식은 비슷하지만, 샘플링하는 픽셀이 멀어질 수록 점점 연해지는 차이점이 있습니다.
위와 같이 일정하게 연한것보다, 멀어질 수록 연해지는것이 더 부드럽게 보입니다.
가우시안 블러 구현 자료를 찾아보면 위와 같이 이상한 수학 공식에 곡선 그래프가 항상 소개됩니다.
간단히 요약하자면, 멀어질 수록 연해지는 강도에 대한 값을 구하는 공식이라고 생각하시면 됩니다.
위 사이트에서 가우시안 곡선 값을 계산할 수 있는데, 위 사각형으로 정렬된 값을 픽셀에 계산하는 방식입니다.
Tip : 가우시안의 뜻은 이 공식을 만든사람의 이름이 칼 프리드리히 가우스라서 그렇습니다.
2 Pass 가우시안블러 쉐이더
float4 BlurHorizontalPassFragment (Varyings i) : SV_TARGET
{
float4 col = 0.0f;
float offsets[] = {
-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0
};
float weights[] = {
0.01621622, 0.05405405, 0.12162162, 0.19459459, 0.22702703,
0.19459459, 0.12162162, 0.05405405, 0.01621622
};
for (int j = 0; j < 9; j++) {
float offset = offsets[j] * 2.0 * _MainTex_TexelSize.x;
col += tex2D(_MainTex, i.uv + float2(offset, 0.0f)) * weights[j];
}
return float4(col.rgb, 1);
}
float4 BlurVerticalPassFragment (Varyings i) : SV_TARGET
{
float4 col = 0.0f;
col=0.0f;
float offsets[] = {
-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0
};
float weights[] = {
0.01621622, 0.05405405, 0.12162162, 0.19459459, 0.22702703,
0.19459459, 0.12162162, 0.05405405, 0.01621622
};
for (int j = 0; j < 9; j++)
{
float offset = offsets[j] * _MainTex_TexelSize.y;
col += tex2D(_MainTex, i.uv + float2(0.0f, offset)) * weights[j];
}
return float4(col.rgb, 1.0);
}
가우시안 블러 또한 앞서 소개한 2 Pass 방식으로 구현했습니다.
코드에서 weight[] 값이 가우시안 커널(공식)에서 미리 계산된 값을 입력한것 입니다.
나머지 코드는 2pass박스 블러와 비슷합니다.
RenderPass
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get(PASS_TAG);
// 임시렌더텍스처 생성
CameraData cameraData = renderingData.cameraData;
RenderTextureDescriptor descriptor = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
cmd.GetTemporaryRT(PROPERTY_TEMPBUFFER_1, descriptor, FilterMode.Bilinear);
cmd.GetTemporaryRT(PROPERTY_TEMPBUFFER_2, descriptor, FilterMode.Bilinear);
// 2 Pass 가우스안블러 렌더링
cmd.Blit(_destination, _tempBuffer_1, _material, 0); // Horizontal
cmd.Blit(_tempBuffer_1, _tempBuffer_2, _material, 1); // Vertical
for (int i = 1; i < blurStep; i++)
{
cmd.Blit(_tempBuffer_2, _tempBuffer_1, _material, 0);
cmd.Blit(_tempBuffer_1, _tempBuffer_2, _material, 1);
}
// 임시렌더텍스처를 화면렌더텍스처에 복사
cmd.Blit(_tempBuffer_2, _destination);
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
렌더패스 코드입니다.
2Pass Box블러와 거의 동일하지만, 추가로 블러를 반복하는 기능을 넣었습니다.
블러를 더 많이 반복 할 수록 블러 퀄리티가 올라갑니다.
결과물 & 비교
가우시안블러가 더 부드러운 블러 느낌이 나고 퍼포먼스 또한 더 가볍습니다.
참고자료
https://chulin28ho.tistory.com/333
https://lovestudycom.tistory.com/entry/Blur-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98
https://catlikecoding.com/unity/tutorials/custom-srp/post-processing/
'Unity > TA' 카테고리의 다른 글
[URP] TEXTURE2D 메크로 (구 sampler2D) (0) | 2022.06.07 |
---|---|
[URP] 반투명 알파소팅 이슈 해결 쉐이더 (2pass) (0) | 2022.06.06 |
URP CustomRenderFeature - 2 Pass 처리 & 가우시안블러 (0) | 2022.04.18 |
URP CustomRenderFeature - 간단 예제 코드 (0) | 2022.04.12 |
URP 커스텀 포스트프로세싱 - 2 개선판 (0) | 2022.04.04 |
WRITTEN BY
- CatDarkGame
Technical Artist dhwlgn12@gmail.com