반응형

이번 포스팅은 URP CustomRenderFeature에서 Box블러Gaussian블러에 대해 분석과 구현 방법에 대해 작성하겠습니다.

 

https://github.com/CatDarkGame/CustomRenderFeatureExample/releases/tag/Blur

 

Release Box & Gaussian Blur · CatDarkGame/CustomRenderFeatureExample

 

github.com

 

 

 

블러 기본 원리


우선 포토샵에서 특정 이미지의 투명도를 낮추고 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 블러


가우시안블러는 박스블러와 블러 방식은 비슷하지만, 샘플링하는 픽셀이 멀어질 수록 점점 연해지는 차이점이 있습니다.

위와 같이 일정하게 연한것보다, 멀어질 수록 연해지는것이 더 부드럽게 보입니다.

 

 

https://m.blog.naver.com/jujbob/221365477294

가우시안 블러 구현 자료를 찾아보면 위와 같이 이상한 수학 공식에 곡선 그래프가 항상 소개됩니다.

간단히 요약하자면, 멀어질 수록 연해지는 강도에 대한 값을 구하는 공식이라고 생각하시면 됩니다.

 

https://dev.theomader.com/gaussian-kernel-calculator/

위 사이트에서 가우시안 곡선 값을 계산할 수 있는데, 위 사각형으로 정렬된 값을 픽셀에 계산하는 방식입니다.

 

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/

 

반응형

WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,