Blur UI는 좋아합니까
저는 엄청 좋아합니다.
하겠습니다.
방침
묘사중인 화면의 텍스쳐로서 얻을 수 있는 GrabPass를 사용하여, 그 녀석에 가우시안 블러를 얹는 쉐이더를 작성합니다.
가우시안 블러
이미 여러 사이트에도 쓰여 있는 초 메이져한 알고리즘이기 때문에 자세히는 설명하지 않겠습니다만, 요점은 「묘사 대상의 픽셀에 대하여, 그 주변 픽셀의 색을 거리에 따른 가중치를 두면서 평균을 얻는다」라는 것이 됩니다. 그리고 이 가중치에 가우스 함수를 이용하기 때문에 가우시안 블러라고 불리는 겁니다. 이 함수를 쓰는 것으로 아주 좋은 느낌의 흐림 효과를 얻을 수 있습니다.
묘사 부하
가우시안 블러를 우직하게 구현하면 화면상의 각각의 픽셀에 대하여 그 주변의 필셀을 참조하게 되기 때문에, 흐림의 강도를 높이면, 상당한 속도로 계산 부하가 높아집니다. 예를들어 흐림 범위 20X20이라면 각 픽셀에 대하여 20X20=400회의 샘플링이 걸립니다. 그렇게 때문에, 계산 부하를 줄이는 연구가 필요합니다.
일반적으로는 다음과 같은 방법으로 실행합니다. 먼저 전체에 가로 방향만으로만 흐림을 주고, 그 화면에 세로 방향으로 흐림을 겁니다. 이렇게 하는 것으로, 샘플링 횟수는 20 + 20 = 40회까지 줄일 수 있습니다. 참고로 이렇게 해도 같은 화면 결과를 얻을 수 있습니다.
여기서, 2회의 흐림 처리를 어떻게 실행할 건가가 문제가 됩니다. 이 사이트에서는 화면이 아닌 특정 텍스쳐에 대한 가우시안 블러를 거는 방법이 소개되어 있는데, 스크립트에서 수동으로 쉐이더를 불러들일 수 있는 Graphics.Blit을 써서, 가로 방향과 세로 방향의 2회분의 Graphics.Blit을 부르도록 설계되어 있습니다. 하지만 이번과 같이 GrapPass를 쓰는 경우, 그 방법으로는 잘 돌아가지 않습니다. (스크립트를 부르는 타이밍에선 GrapPass가 작동하지 않기 때문. 아무래도 GrapPass는 카메라에서의 화면으로만 기능하는 것 같습니다. 애초에 카메라의 화면 타이밍 이외에서는 "그 시점에 카메라가 묘사한 화면"이 존재하지 않기 때문에 당연하지만요)
그렇기 때문에, GrapPass를 사용한 이상은 의지로라도 스크립트 없이 구현하지 않으면 안됩니다. 그래서 멀티 패스 쉐이더를 쓰ㅃ니다.
멀티 패스 쉐이더라고 해도 대단한 것은 아닙니다. 요는 하나의 쉐이더에 두 개의 처리(pass)를 넣는 것 뿐입니다. 세로 방향의 흐림과 가로 방향의 흐림을 각각 작성하면 됩니다.
먼저는 GrapPass의 아랫부분을 준비
일단은, GrapPass로 화면을 텍스쳐로 얻고 그대로 표시하는 쉐이더를 작성합니다. 블러 쉐이더는 이것을 원형으로 작성해나갑니다.
Shader "Custom/Blur" {
Properties
{
_MainTex("Texture", 2D) = "white" {}
}
SubShader
{
Tags{ "Queue" = "Transparent" }
GrabPass
{
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
};
struct v2f
{
float4 grabPos : TEXCOORD0;
float4 pos : SV_POSITION;
float4 vertColor : COLOR;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.pos);
o.vertColor = v.color;
return o;
}
sampler2D _GrabTexture;
half4 frag(v2f i) : SV_Target
{
half4 col = tex2Dproj(_GrabTexture, i.grabPos);
return col;
}
ENDCG
}
}
} |
가로 방향의 블러를 작성
파라메터
Properties에 가우시안 블러로 주위 픽셀을 취득하는 반경을 파라메터로서 추가합니다.
Properties
{
_MainTex("Texture", 2D) = "white" {}
_Blur("Blur", Float) = 10 } |
프래그먼트 쉐이더
...(前略)
sampler2D _GrabTexture;
fixed4 _GrabTexture_TexelSize;
float _Blur;
half4 frag(v2f i) : SV_Target
{
float blur = _Blur;
blur = max(1, blur);
fixed4 col = (0, 0, 0, 0);
float weight_total = 0;
[loop]
for (float x = -blur; x <= blur; x += 1)
{
float distance_normalized = abs(x / blur);
float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
weight_total += weight;
col += tex2Dproj(_GrabTexture, i.grabPos + float4(x * _GrabTexture_TexelSize.x, 0, 0, 0)) * weight;
}
col /= weight_total;
return col;
} (後略)... |
해설
내용이 많기 때문에 해설합니다.
[loop]
for (float x = -blur; x <= blur; x += 1) { |
블러 반경에 따라, 현재 위치를 중심으로 왼쪽에서 오른쪽으로 픽셀을 흐리게 하는 루프입니다.
float distance_normalized = abs(x / blur); |
블러 중심에서의 거리를 구합니다. 0~1의 범위로 정규화하고 있기 때문에, 블러 반경이 변화해도 똑같이 처리할 수 있게 됩니다.
float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0); weight_total += weight; |
가우시안 블러의 알고리즘에 따라 영향력을 계산합니다.
가우시안 블러의 영향력에 사용하는 함수는 이런 느낌의 형태입니다만, y는 완전한 0은 되지 않으므로, 능숙하게 계수를 조정해서 형태를 맞춰야만 합니다. 대체로 블러 반경의 양끝에 영향력이 0.1 이하가 되도록 조정한 결과, 5.0이 걸려 있습니다.
계산한 영향력은 나중에 평균을 얻을 때 사용하기 때문에, weight_total에 가산해둡니다.
col += tex2Dproj(_GrabTexture, i.grabPos + float4(x * _GrabTexture_TexelSize.x, 0, 0, 0)) * weight; |
정형한 _GrapTexture_TexelSize.xy에는, GrabPass로 얻은 화면의 텍스쳐 사이즈의 역수가 들어갑니다. 이게 그야말로 1픽셀분의 uv공간상의 거리가 되는 겁니다. 그리고는 이것을 원래의 _GrapPass를 참조하여, 영향력을 더해 가산하면 됩니다.
그리고 가산한 색을 영향력의 합계로 나누면, 영향력을 고려한 평균값이 구해집니다.
세로 방향 블러를 작성하기
세로 방향 블러를 추가합니다. 가로 방향 블러를 더한 결과의 이미지에, 더해서 세로 방향 블러를 더할 필요가 있기 때문에, GrapPass와 Pass를 하나씩 보충해서 써줍니다. Pass의 내용은 거의 가로 방향 블러와 똑같기 때문에 조금 중복 느낌이 있지만, 이렇게 해야만 하기 때문에 포기하고 작성합니다.
...(前略)
col /= weight_total;
return col;
}
ENDCG
}
GrabPass
{
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
};
struct v2f
{
float4 grabPos : TEXCOORD0;
float4 pos : SV_POSITION;
float4 vertColor : COLOR;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.pos);
o.vertColor = v.color;
return o;
}
sampler2D _GrabTexture;
fixed4 _GrabTexture_TexelSize;
float _Blur;
half4 frag(v2f i) : SV_Target
{
float blur = _Blur;
blur = max(1, blur);
fixed4 col = (0, 0, 0, 0);
float weight_total = 0;
[loop]
for (float y = -blur; y <= blur; y += 1)
{
float distance_normalized = abs(y / blur);
float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
weight_total += weight;
col += tex2Dproj(_GrabTexture, i.grabPos + float4(0, y * _GrabTexture_TexelSize.y, 0, 0)) * weight;
}
col /= weight_total;
return col;
}
ENDCG
}
}
}
|
다 하셨나요?
전문
Shader "Custom/Blur"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_Blur("Blur", Float) = 10
}
SubShader
{
Tags{ "Queue" = "Transparent" }
GrabPass
{
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
};
struct v2f
{
float4 grabPos : TEXCOORD0;
float4 pos : SV_POSITION;
float4 vertColor : COLOR;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.pos);
o.vertColor = v.color;
return o;
}
sampler2D _GrabTexture;
fixed4 _GrabTexture_TexelSize;
float _Blur;
half4 frag(v2f i) : SV_Target
{
float blur = _Blur;
blur = max(1, blur);
fixed4 col = (0, 0, 0, 0);
float weight_total = 0;
[loop]
for (float x = -blur; x <= blur; x += 1)
{
float distance_normalized = abs(x / blur);
float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
weight_total += weight;
col += tex2Dproj(_GrabTexture, i.grabPos + float4(x * _GrabTexture_TexelSize.x, 0, 0, 0)) * weight;
}
col /= weight_total;
return col;
}
ENDCG
}
GrabPass
{
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
fixed4 color : COLOR;
};
struct v2f
{
float4 grabPos : TEXCOORD0;
float4 pos : SV_POSITION;
float4 vertColor : COLOR;
};
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.grabPos = ComputeGrabScreenPos(o.pos);
o.vertColor = v.color;
return o;
}
sampler2D _GrabTexture;
fixed4 _GrabTexture_TexelSize;
float _Blur;
half4 frag(v2f i) : SV_Target
{
float blur = _Blur;
blur = max(1, blur);
fixed4 col = (0, 0, 0, 0);
float weight_total = 0;
[loop]
for (float y = -blur; y <= blur; y += 1)
{
float distance_normalized = abs(y / blur);
float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
weight_total += weight;
col += tex2Dproj(_GrabTexture, i.grabPos + float4(0, y * _GrabTexture_TexelSize.y, 0, 0)) * weight;
}
col /= weight_total;
return col;
}
ENDCG
}
} } |
주의점
이번의 흐림 처리는 화면 전체가 아닌 특정 영역에만 대해서 실행하고 있기 때문에, 영역의 경계 부분에서 올바른 결과를 얻지 못할 경우도 있습니다. 실용시 유의하시기 바랍니다.
참고
Unityでガウシアンブラーを実装する - e.blog
http://edom18.hateblo.jp/entry/2018/12/30/171214
ShaderLab: GrabPass
https://docs.unity3d.com/ja/current/Manual/SL-GrabPass.html
원문: https://qiita.com/ruccho_vector/items/651f760b3240a253bd1d