Microsoft MVP성태의 닷넷 이야기
글쓴 사람
홈페이지
첨부 파일

Unity로 실습하는 Shader (7) - Blur (평균값, 가우스, 중간값) 필터

지난 글에서,

Unity로 실습하는 Shader (6) - Mosaic Shading
; https://www.sysnet.pe.kr/2/0/11619

for 루프를 이용한 처리를 봤는데, 그렇다면 texture를 본연의 이미지 데이터라 보고 OpenCV를 통해했던 이미지 프로세싱까지 - 가령 mask 처리 같은 것도 가능하다는 시나리오가 나옵니다. 그중에서 3가지 스무딩 효과를 Unity shader로 구현해 보겠습니다.




우선, OpenCV의 box filter인 평균값 필터를 다음과 같이 구현해 볼 수 있습니다.

fixed4 frag(v2f i) : SV_Target
{
    float maskWidth = 5; // 외부 변수 처리 권장
    float maskOffset = -floor(maskWidth / 2.0);
            
    float homogeneousBlurFilter = 1.0 / (maskWidth * maskWidth);
    float2 texelSize = _MainTex_TexelSize;

    float2 xy = UVtoXY(i.uv, texelSize);
    float4 color = float4(0.0, 0.0, 0.0, 0.0);

    for (float x = 0; x < maskWidth; x++)
    {
        float offsetX = x + maskOffset;

        for (float y = 0; y < maskWidth; y++)
        {
            float offsetY = y + maskOffset;

            float2 newXY = float2(xy.x + offsetX, xy.y + offsetY);
            float2 newUV = XYtoUV(newXY, texelSize);

            color += (tex2D(_MainTex, newUV) * homogeneousBlurFilter);
        }
    }

    color = color * i.diffuse + i.specular;
    return color;
}

위의 코드는 maskWidth == 5이므로 5x5 크기의 1.0 / 25 평균값을 지정한 것과 같습니다.

${
\begin{split}K = \frac{1}{25} \begin{bmatrix} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{bmatrix}\end{split}
}$


이것을 적용해 보면, 지구본의 이미지 texture가 너무 커서(2048 * 1024) 별로 차이가 안 납니다.

[그림 1: 왼쪽은 원본, 오른쪽은 5x5 box filter가 적용된 셰이딩]
blur_effect_shader_1.png

변수를 조정해서 25로 늘리면,

maskWidth = 25;

즉, 25x25 크기의 1.0 / 625 필터를 적용하면 제법 눈에 띄는 blurring 효과가 나오는 것을 확인할 수 있습니다.

[그림 1: 왼쪽은 원본, 오른쪽은 25x25 box filter가 적용된 셰이딩]
blur_effect_shader_2.png

그나저나, 실습을 위해 texture 크기를 2048 * 1024 크기에서 512 * 256으로 해상도를 낮춘 지구본 이미지를 사용하겠습니다. 다음은 512 * 256이었을 때 고로 셰이딩(왼쪽)과 5x5 평균값 필터를 적용했을 때의 결과를 보여줍니다.

blur_effect_shader_3.png

위의 코드를 기반으로 하면, 가중 평균 값 필터(weighted mean filter)도 어렵지 않게 적용할 수 있습니다.




두 번째로, blurring 효과 하면 유명한 가우스 필터를 안 해볼 수 없습니다. ^^ 우선, 멋있게 1차원과 2차원 가우스 분포 함수를 써 두지만,

${ G(x) = \frac{1}{{\sigma \sqrt {2\pi } }}e^{{{ -{x}^2 } \mathord{\left/ \right. } {2\sigma ^2 }}}
}$


${ G(x, y) = \frac{1}{{\sigma ^2 {2\pi } }}e^{{{ - \left( x^2 + y^2 \right) } \mathord{\left/ \right. } {2\sigma ^2 }}}
}$


tex2D 함수는 2차원 texture를 대상으로 하기 때문에 2차원 가우스 분포 함수가 적용됩니다. 따라서, σ 값에 따라 적절한 gaussian kernel을 생성하는데,

Gaussian Kernel Calculator
; http://dev.theomader.com/gaussian-kernel-calculator/

일례로 σ = 1.0으로 5x5 크기의 커널을 사용해 다음과 같이 코딩을 할 수 있습니다.

fixed4 frag(v2f i) : SV_Target
{
    // sigma = 1.0
    float gaussian5x5BlurFilter[25] =
    {
        0.003765, 0.015019, 0.023792, 0.015019, 0.003765,
        0.015019, 0.059912, 0.094907, 0.059912, 0.015019,
        0.023792, 0.094907, 0.150342, 0.094907, 0.023792,
        0.015019, 0.059912, 0.094907, 0.059912, 0.015019,
        0.003765, 0.015019, 0.023792, 0.015019, 0.003765,
    };

    float maskWidth = 5;
    float maskOffset = -floor(maskWidth / 2.0);
            
    float2 texelSize = _MainTex_TexelSize;

    float2 xy = UVtoXY(i.uv, texelSize);
    float4 color = float4(0.0, 0.0, 0.0, 0.0);

    for (float y = 0; y < maskWidth; y++)
    {
        float offsetY = y + maskOffset;

        for (float x = 0; x < maskWidth; x++)
        {
            float offsetX = x + maskOffset;

            float2 newXY = float2(xy.x + offsetX, xy.y + offsetY);
            float2 newUV = XYtoUV(newXY, texelSize);

            fixed filterPos = y * 5 + x;

            float filter = gaussian5x5BlurFilter[filterPos];
            color += (tex2D(_MainTex, newUV) * filter);
        }
    }

    color = color * i.diffuse + i.specular;
    return color;
}

아래의 그림은 좌측부터, 원본, 5x5 평균 값 필터링, 5x5 가우스 필터링 효과를 보여줍니다.

blur_effect_shader_4.png

참고로, sigma = 25.0으로 9x9의 mask를 적용하면,

// sigma = 25.0
float gaussian9x9BlurFilter[81] =
{
    0.012162, 0.012231, 0.01228 , 0.012309, 0.012319, 0.012309, 0.01228 , 0.012231, 0.012162,
    0.012231, 0.012299, 0.012349, 0.012378, 0.012388, 0.012378, 0.012349, 0.012299, 0.012231,
    0.01228 , 0.012349, 0.012398, 0.012428, 0.012438, 0.012428, 0.012398, 0.012349, 0.01228 ,
    0.012309, 0.012378, 0.012428, 0.012458, 0.012468, 0.012458, 0.012428, 0.012378, 0.012309,
    0.012319, 0.012388, 0.012438, 0.012468, 0.012478, 0.012468, 0.012438, 0.012388, 0.012319,
    0.012309, 0.012378, 0.012428, 0.012458, 0.012468, 0.012458, 0.012428, 0.012378, 0.012309,
    0.01228 , 0.012349, 0.012398, 0.012428, 0.012438, 0.012428, 0.012398, 0.012349, 0.01228 ,
    0.012231, 0.012299, 0.012349, 0.012378, 0.012388, 0.012378, 0.012349, 0.012299, 0.012231,
    0.012162, 0.012231, 0.01228 , 0.012309, 0.012319, 0.012309, 0.01228 , 0.012231, 0.012162,
};

float maskWidth = 9;

다음과 같이 좀 더 blurring된 것을 볼 수 있습니다.

[좌: 원본, 중간: 평균값, 우: σ = 25.0, 9x9 gaussian filter]
blur_effect_shader_5.png

그런데 2차원 가우시안 함수는 x와 y 방향에 대해 각각 1차원의 가우시안 함수의 곱으로 나타낼 수 있으므로 NxN 마스크가 아닌, 1xN 마스크를 x축과 (2 pass를 이용해) y 축 방향으로 사용할 수 있습니다.(단일 객체로 2 pass를 지정해 blurring 처리하는 것이 가능할까요?)




마지막으로, 중간값 필터(Median Filter)를 적용해 보겠습니다. 중간값이 의미하는 것처럼, mask에 속하는 영역 중 중간값에 해당하는 컬러를 선택하는 것이기 때문에 정렬이 필요한 기능이기도 합니다. 중간값을 반환하기 위한 정렬은 간단하게 다음과 같이 해결할 수 있습니다.

float4 getMedian(float4 buf[25], fixed len)
{
    float4 temp;

    // 버블 정렬
    for (float i = 0; i < len - 1; i ++)
    {
        for (float j = i + 1 ; j < len; j ++)
        {
            if (colorToFloat(buf[i]) < colorToFloat(buf[j]))
            {
                temp = buf[i];
                buf[i] = buf[j];
                buf[j] = temp;
            }
        }
    }

    return buf[len / 2]; // 중간값 반환
}

아쉬운 것은 함수의 인자로 배열을 전달할 때 반드시 크기를 명시해야 한다는 점입니다.

// 반드시 배열 크기 명시
float4 getMedian(float4 buf[25], fixed len)

// 아래와 같이 전달하면 (컴파일 오류 없이) 동작하지 않음
float4 getMedian(float4 buf[], fixed len)

그래서 getMedian과 같이 함수를 만들면 배열 크기에 따른 함수를 별도로 만들어줘야 합니다. 이하 나머지 코드는 평균값 필터와 크게 다르지 않습니다.

fixed4 frag(v2f i) : SV_Target
{
    float maskWidth = 5;
    float4 colorArray[5 * 5];

    float maskOffset = -floor(maskWidth / 2.0);
            
    float2 texelSize = _MainTex_TexelSize;

    float2 xy = UVtoXY(i.uv, texelSize);
    float4 color = float4(0.0, 0.0, 0.0, 0.0);

    for (float x = 0; x < maskWidth; x++)
    {
        float offsetX = x + maskOffset;

        for (float y = 0; y < maskWidth; y++)
        {
            float offsetY = y + maskOffset;

            float2 newXY = float2(xy.x + offsetX, xy.y + offsetY);
            float2 newUV = XYtoUV(newXY, texelSize);

            colorArray[y * maskWidth + x] = tex2D(_MainTex, newUV);
        }
    }

    float4 col = getMedian(colorArray, maskWidth * maskWidth);
    color = col * i.diffuse + i.specular;

    return color;
}

실행 결과는 다음과 같습니다.

[좌측부터 원본, 5x5 평균값 필터, σ = 25.0 and 9x9 가우스 필터, 5x5 중간값 필터]
blur_effect_shader_6.png

참고로, 아래의 소스 코드를 보면 min, max를 활용해 (정렬 없이) 3x3 크기의 중간값 필터를 구현하고 있습니다.

3x3 Median - Morgan McGuire and Kyle Whitson
; http://casual-effects.com/research/McGuire2008Median/median.pix

(첨부 파일은 이 글에서 구현한 3가지 shader 소스 코드를 모두 포함합니다.)




shader에서 컬러를 비교하려고 다음과 같이 코딩을 하면,

fixed4 frag(v2f i) : SV_TARGET
{
    float4 color1 = tex2D(_MainTex, uv1);
    float4 color2 = tex2D(_MainTex, uv2);

    if (color1 < color2)
    {
    }
}

컴파일 오류가 발생합니다.

if statement conditional expressions must evaluate to a scalar

사실 단색이 아닌 color 값 비교라는 게 좀 이상하긴 하지만, 다음과 같이 우회해 볼 수는 있습니다.

float colorToFloat(float4 color)
{
    // tex2D가 반환하는 컬러는 0 ~ 1 사이의 값임.
    return color.r + color.g + color.b;
}

fixed4 frag(v2f i) : SV_TARGET
{
    float4 color1 = tex2D(_MainTex, uv1);
    float4 color2 = tex2D(_MainTex, uv2);

    if (colorToFloat(color1) < colorToFloat(color2))
    {
    }
}




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]





[최초 등록일: ]
[최종 수정일: 8/3/2018 ]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer@outlook.com

비밀번호

댓글 쓴 사람
 




1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...
NoWriterDateCnt.TitleFile(s)
11655정성태8/18/20181634사물인터넷: 28. 저항과 병렬로 연결한 LED파일 다운로드1
11654정성태8/18/20181305사물인터넷: 27. 병렬 회로의 저항, 전압 및 전류파일 다운로드1
11653정성태8/18/20181137사물인터넷: 26. 입력 전압에 따른 LED의 전압/저항 변화파일 다운로드1
11652정성태9/11/20181023사물인터넷: 25. 컬렉터 9V, 베이스에 5V와 3.3V 전압으로 테스트하는 C1815 트랜지스터파일 다운로드1
11651정성태9/4/20182039사물인터넷: 24. 9V 전압에서 테스트하는 C1815 트랜지스터파일 다운로드3
11650정성태8/18/20181124사물인터넷: 23. 가변저항으로 분압파일 다운로드1
11649정성태8/14/20181235사물인터넷: 22. 저항에 따른 전류 테스트파일 다운로드1
11648정성태10/17/20181701사물인터넷: 21. 퓨즈를 이용한 회로 보호파일 다운로드3
11647정성태8/8/20181386오류 유형: 476. 음수의 음수는 여전히 음수가 되는 수(절대값이 음수인 수)
11646정성태8/8/20181106오류 유형: 475. gacutil.exe 실행 시 "Failure initializing gacutil" 오류 발생
11645정성태8/8/20181227오류 유형: 474. 닷넷 COM+ - Failed to load the runtime. [1]
11644정성태8/8/20181737디버깅 기술: 118. windbg - 닷넷 개발자를 위한 MEX Debugging Extension 소개
11643정성태8/12/20181838사물인터넷: 20. 아두이노 레오나르도 R3 호환 보드의 3.3v 핀의 LED 전압/전류 테스트 [1]파일 다운로드1
11642정성태8/3/20181727Graphics: 20. Unity - LightMode의 ForwardBase에 따른 _WorldSpaceLightPos0 값 변화
11641정성태8/3/20182383Graphics: 19. Unity로 실습하는 Shader (10) - 빌보드 구현파일 다운로드1
11640정성태8/3/20181894Graphics: 18. Unity - World matrix(unity_ObjectToWorld)로부터 Position, Rotation, Scale 값을 복원하는 방법파일 다운로드1
11639정성태8/2/20181449디버깅 기술: 117. windbg - 덤프 파일로부터 추출한 DLL을 참조하는 방법
11638정성태8/2/20181316오류 유형: 473. windbg - 덤프 파일로부터 추출한 DLL 참조 시 "Resolved file has a bad image, no metadata, or is otherwise inaccessible." 빌드 오류
11637정성태8/1/20181654Graphics: 17. Unity - World matrix(unity_ObjectToWorld)로부터 TRS(이동/회전/크기) 행렬로 복원하는 방법파일 다운로드1
11636정성태8/1/20182422Graphics: 16. 3D 공간에서 두 점이 이루는 각도 구하기파일 다운로드1
11635정성태8/1/20181411오류 유형: 472. C# 컴파일 오류 - Your project is not referencing the ".NETFramework,Version=v3.5" framework.
11634정성태8/1/20181350.NET Framework: 790. .NET Thread 상태가 Cooperative일 때 GC hang 현상 재현 방법파일 다운로드1
11633정성태7/29/20182068Graphics: 15. Unity - shader의 World matrix(unity_ObjectToWorld)를 수작업으로 구성
11632정성태7/28/20183994Graphics: 14. C# - Unity에서 캐릭터가 바라보는 방향을 기준으로 카메라의 위치 이동 및 회전하는 방법
11631정성태7/27/20183152Graphics: 13. Unity로 실습하는 Shader (9) - 투명 배경이 있는 텍스처 입히기
11630정성태7/27/20181846개발 환경 구성: 391. (GitHub 등과 직접 연동해) 소스 코드 디버깅을 쉽게 해 주는 SourceLink [2]
1  2  3  4  5  6  7  8  9  10  11  12  13  14  [15]  ...