簡(jiǎn)介
游戲都追求畫(huà)面感,而個(gè)人感覺(jué)后處理是最有效的表現畫(huà)面感的方式之一(雖然比較收費)。我以前玩過(guò)這種模糊效果。今天我來(lái)看看另一個(gè)模糊效果,徑向模糊。這種效果對提高畫(huà)面感非常有幫助,特別是在快速動(dòng)作突然加速的瞬間,或是老板吼出來(lái)的畫(huà)面效果。所以我第一次想到天涯明月刀的光影工作瞬間飛升加速,使用徑向模糊使光影工作突然加速。

“王者的榮耀”也有徑向模糊的效果。

徑向模糊效果的原理
放射狀模糊效果是后處理,后處理的原理不多,用描繪的屏幕圖像進(jìn)行全畫(huà)面操作。首先,看圖,從中心部分向外側擴大,感覺(jué)畫(huà)面向外延伸。因此,在fragment階段,之前的文章為了在線(xiàn)效果的后處理時(shí)露出輪廓,可以使用模糊效果對像素和周?chē)南袼剡M(jìn)行加權平均這樣的想法進(jìn)行處理。另一方面,關(guān)于放射狀模糊也一樣,關(guān)于各像素點(diǎn),雖然不一定是畫(huà)面的正中心,自己可以指定)求出1個(gè)方向,從中心朝向該像素點(diǎn)的方向是放射狀的模糊方向,其次是現在的像素點(diǎn)以及放射狀。進(jìn)一步將幾個(gè)點(diǎn)作為抽樣點(diǎn)取得,抽樣點(diǎn)越靠近中心越緊密,變得疏松。最后,這個(gè)像素點(diǎn)的輸出是這些抽樣點(diǎn)的平均值。.像這樣,在靠近中心點(diǎn)的位置,取樣距離小,幾乎為0。沒(méi)有模糊,距離越近取樣距離越大,圖像變得模糊。
放射狀模糊的原理如下.

徑向模糊在Unity下的實(shí)現
了解原理后,我們在Unity下實(shí)現了1版放射狀模糊效果.在后期處理的組合中,C#腳本部分RadialBlurEffect類(lèi)繼承了PostEffectBase類(lèi)。這個(gè)等級在以前的報道中進(jìn)行了說(shuō)明,但是這里不顯示。
RadialBlurEffect腳本:
using UnityEngine;
 
public class RadialBlurEffect : PostEffectBase {
 
    //模糊程度,不能過(guò)高
    [Range(0,0.05f)]
    public float blurFactor = 1.0f;
    //模糊中心(0-1)屏幕空間,默認為中心點(diǎn)
    public Vector2 blurCenter = new Vector2(0.5f, 0.5f);
 
void OnRenderImage (RenderTexture source, RenderTexture destination)
    {
        if (_Material)
        {
            _Material.SetFloat("_BlurFactor", blurFactor);
            _Material.SetVector("_BlurCenter", blurCenter);
            Graphics.Blit(source, destination, _Material);
        }
        else
        {
            Graphics.Blit(source, destination);
        }   
} 
}
RadialBlurShader1:
Shader "ApcShader/PostEffect/RadialBlurShader1" 
{
Properties 
{
_MainTex ("Base (RGB)", 2D) = "white" {}
}
 
CGINCLUDE
uniform sampler2D _MainTex;
uniform float _BlurFactor; //模糊強度(0-0.05)
uniform float4 _BlurCenter; //模糊中心點(diǎn)xy值(0-1)屏幕空間
#include "UnityCG.cginc"
#define SAMPLE_COUNT 6 //迭代次數
 
fixed4 frag(v2f_img i) : SV_Target
{
//模糊方向為模糊中點(diǎn)指向邊緣(當前像素點(diǎn)),而越邊緣該值越大,越模糊
float2 dir = i.uv - _BlurCenter.xy;
float4 outColor = 0;
//采樣SAMPLE_COUNT次
for (int j = 0; j < SAMPLE_COUNT; ++j)
{
//計算采樣uv值:正常uv值+從中間向邊緣逐漸增加的采樣距離
float2 uv = i.uv + _BlurFactor * dir * j;
outColor += tex2D(_MainTex, uv);
}
//取平均值
outColor /= SAMPLE_COUNT;
return outColor;
}
ENDCG
 
SubShader
{
Pass
{
ZTest Always
Cull Off
ZWrite Off
Fog{ Mode off }
 
//調用CG函數
CGPROGRAM
//使效率更高的編譯宏
#pragma fragmentoption ARB_precision_hint_fastest 
//vert_img是在UnityCG.cginc中定義好的,當后處理vert階段計算常規,可以直接使用自帶的vert_img
#pragma vertex vert_img
#pragma fragment frag 
ENDCG
}
}
Fallback off
}
我們在尋找場(chǎng)景并測試其效果。以下是未啟用“徑向模糊”效果的原始場(chǎng)景。

開(kāi)啟徑向模糊效果后:

調整模糊強度:

徑向模糊的優(yōu)化
徑向模糊和高斯模糊,均值模糊等模糊效果一樣,都是采樣次數越高,模糊效果越好,但是采樣次數高了,性能就下去了,尤其是在移動(dòng)設備上,GPU不強,但是分辨率極高,后處理這種全屏紋理采樣及其耗費性能。所以,優(yōu)化很重要。我們在高斯模糊以及Bloom效果中使用了降分辨率的操作,對于徑向模糊,一樣實(shí)用。優(yōu)化的思路如下:首先,我們將圖像渲染到一張降低了分辨率的RT上,然后使用這個(gè)降低了分辨率的RT進(jìn)行上面的模糊處理;最后再將這個(gè)RT與原始圖像進(jìn)行插值操作。這樣會(huì )多一個(gè)Pass(一個(gè)Draw Call),但是由于降低了分辨率,仍然對性能會(huì )有較好的提升。下面附上更改后的徑向模糊代碼。
RadialBlurEffect2腳本:
using UnityEngine;
 
public class RadialBlurEffect2 : PostEffectBase {
 
    //模糊程度,不能過(guò)高
    [Range(0,0.1f)]
    public float blurFactor = 1.0f;
    //清晰圖像與原圖插值
    [Range(0.0f, 2.0f)]
    public float lerpFactor = 0.5f;
    //降低分辨率
    public int downSampleFactor = 2;
    //模糊中心(0-1)屏幕空間,默認為中心點(diǎn)
    public Vector2 blurCenter = new Vector2(0.5f, 0.5f);
 
void OnRenderImage (RenderTexture source, RenderTexture destination)
    {
        if (_Material)
        {
            //申請兩塊降低了分辨率的RT
            RenderTexture rt1 = RenderTexture.GetTemporary(source.width >> downSampleFactor, source.height >> downSampleFactor, 0, source.format);
            RenderTexture rt2 = RenderTexture.GetTemporary(source.width >> downSampleFactor, source.height >> downSampleFactor, 0, source.format);
            Graphics.Blit(source, rt1);
 
            //使用降低分辨率的rt進(jìn)行模糊:pass0
            _Material.SetFloat("_BlurFactor", blurFactor);
            _Material.SetVector("_BlurCenter", blurCenter);
            Graphics.Blit(rt1, rt2, _Material, 0);
 
            //使用rt2和原始圖像lerp:pass1
            _Material.SetTexture("_BlurTex", rt2);
            _Material.SetFloat("_LerpFactor", lerpFactor);
            Graphics.Blit(source, destination, _Material, 1);
 
            //釋放RT
            RenderTexture.ReleaseTemporary(rt1);
            RenderTexture.ReleaseTemporary(rt2);
        }
        else
        {
            Graphics.Blit(source, destination);
        }   
} 
}

RadialBlurEffect2.shader:

Shader "ApcShader/PostEffect/RadialBlurShader2" 
{
Properties 
{
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurTex("Blur Tex", 2D) = "white"{}
}
 
CGINCLUDE
uniform sampler2D _MainTex;
uniform sampler2D _BlurTex;
uniform float _BlurFactor; //模糊強度(0-0.05)
uniform float _LerpFactor;  //插值的強度(0-1)
uniform float4 _BlurCenter; //模糊中心點(diǎn)xy值(0-1)屏幕空間
float4 _MainTex_TexelSize;
#include "UnityCG.cginc"
#define SAMPLE_COUNT 6 //迭代次數
 
fixed4 frag_blur(v2f_img i) : SV_Target
{
//模糊方向為模糊中點(diǎn)指向邊緣(當前像素點(diǎn)),而越邊緣該值越大,越模糊
float2 dir = i.uv - _BlurCenter.xy;
float4 outColor = 0;
//采樣SAMPLE_COUNT次
for (int j = 0; j < SAMPLE_COUNT; ++j)
{
//計算采樣uv值:正常uv值+從中間向邊緣逐漸增加的采樣距離
float2 uv = i.uv + _BlurFactor * dir * j;
outColor += tex2D(_MainTex, uv);
}
//取平均值
outColor /= SAMPLE_COUNT;
return outColor;
}
 
//定義最后插值使用的結構體
struct v2f_lerp
{
float4 pos : SV_POSITION;
float2 uv1 : TEXCOORD0; //uv1
float2 uv2 : TEXCOORD1; //uv2
};
v2f_lerp vert_lerp(appdata_img v)
{
v2f_lerp o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
o.uv1 = v.texcoord.xy;
o.uv2 = v.texcoord.xy;
//dx中紋理從左上角為初始坐標,需要反向(在寫(xiě)rt的時(shí)候需要注意)
#if UNITY_UV_STARTS_AT_TOP
if (_MainTex_TexelSize.y < 0)
o.uv2.y = 1 - o.uv2.y;
#endif
return o;
}
 
fixed4 frag_lerp(v2f_lerp i) : SV_Target
{
float2 dir = i.uv1 - _BlurCenter.xy;
float dis = length(dir);
fixed4 oriTex = tex2D(_MainTex, i.uv1);
fixed4 blurTex = tex2D(_BlurTex, i.uv2);
//按照距離乘以插值系數在原圖和模糊圖之間差值
return lerp(oriTex, blurTex, _LerpFactor * dis);
}
ENDCG
 
SubShader
{
//Pass 0 模糊操作
Pass
{
ZTest Always
Cull Off
ZWrite Off
Fog{ Mode off }
 
//調用CG函數
CGPROGRAM
//使效率更高的編譯宏
#pragma fragmentoption ARB_precision_hint_fastest 
//vert_img是在UnityCG.cginc中定義好的,當后處理vert階段計算常規,可以直接使用自帶的vert_img
#pragma vertex vert_img
#pragma fragment frag_blur 
ENDCG
}
 
//Pass 1與原圖插值操作
Pass
{
ZTest Always
Cull Off
ZWrite Off
Fog{ Mode off }
 
//調用CG函數
CGPROGRAM
//使效率更高的編譯宏
#pragma fragmentoption ARB_precision_hint_fastest 
//vert_img是在UnityCG.cginc中定義好的,當后處理vert階段計算常規,可以直接使用自帶的vert_img
#pragma vertex vert_lerp
#pragma fragment frag_lerp 
ENDCG
}
}
Fallback off

還是剛才的場(chǎng)景,使用新版本的后處理效果的結果如下:

這里我們降低了2倍分辨率,極大地降低了采樣帶來(lái)的消耗。雖然效果也打了些折扣,不過(guò)還是可以湊合看的,哈哈。
?
UNITY_UV_STARTS_AT_TOP宏
?
我們寫(xiě)后處理shader時(shí)經(jīng)常遇到的一個(gè)問(wèn)題就是有時(shí)候開(kāi)啟后處理效果時(shí)屏幕上下顛倒了(或者疊加上去的部分上下顛倒了),在之前的屏幕水波紋效果中就遇到了類(lèi)似的問(wèn)題。那么有時(shí)候是什么時(shí)候呢?需要滿(mǎn)足幾個(gè)條件,第一,使用DX渲染器時(shí)(也就是在PC平臺);第二,開(kāi)啟了抗鋸齒(AA);第三,開(kāi)啟了后處理并且在后處理中使用了除了MainTex外的屏幕RT(或者后處理中跟屏幕像素相關(guān)的值直接傳入shader),比如上面的shader中我們除了MainTex外額外傳入了一張BlurTex,我們仍然用MainTex的uv來(lái)處理時(shí)。同時(shí)滿(mǎn)足了上面三點(diǎn),就會(huì )出現屏幕上下顛倒的現象。如下圖所示:

本人第一次遇到這個(gè)問(wèn)題時(shí)瞬間想到《艾?!纷詈笈园椎哪嵌蜝oss戰,旁白直接把屏幕倒轉過(guò)來(lái),在《地獄邊境》中也有類(lèi)似的上下顛倒的關(guān)卡。shader能做的東西好多,之前一直沒(méi)想到過(guò)那種上下屏幕顛倒是怎么做的,再仔細一想,其實(shí)只要在正常做,增加一個(gè)后處理,把采樣的uv的y坐標反過(guò)來(lái),就可以了。
?
那么,為什么會(huì )出現這種屏幕翻轉的問(wèn)題,又為什么滿(mǎn)足這幾個(gè)條件才會(huì )有這種屏幕翻轉的問(wèn)題呢?引用一段官方文檔上的解釋?zhuān)?/span>
Direct3D-like: The coordinate is 0 at the top and increases downward. This applies to Direct3D, Metal and consoles.
OpenGL-like: The coordinate is 0 at the bottom and increases upward. This applies to OpenGL and OpenGL ES.
This difference tends not to have any effect on your project, other than when rendering into a Render Texture. When rendering into a Texture on a Direct3D-like platform, Unity internally flips rendering upside down. This makes the conventions match between platforms, with the OpenGL-like platform convention the standard.Image Effects and rendering in UV space are two common cases in the Shaders where you need to take action to ensure that the different coordinate conventions do not create problems in your project.
When you use Image Effects and anti-aliasing, the resulting source Texture for an Image Effect is not flipped to match the OpenGL-like platform convention. In this case, Unity renders to the screen to get anti-aliasing and then resolves rendering into a Render Texture for further processing with an Image Effect.
If your Image Effect is a simple one that processes one Render Texture at a time, Graphics.Blit deals with the inconsistent coordinates. However, if you’re processing more than one Render Texture together in your Image Effect, the Render Textures are likely to come out at different vertical orientations in Direct3D-like platforms and when you use anti-aliasing. To standardise the coordinates, you need to manually “flip” the screen Texture upside down in your Vertex Shader so that it matches the OpenGL-like coordinate standard.
由于DX和OpenGL之間的區別,Unity為了跨平臺,為我們處理了兩個(gè)圖形API紋理坐標不同的問(wèn)題,但是不是任何時(shí)候都為我們自動(dòng)處理,當我們用后處理(也就是寫(xiě)入RT)并且開(kāi)啟了抗鋸齒的時(shí)候,就不會(huì )為我們翻轉。Unity就是這么設定的,當有這種情況的話(huà)我們就需要自己處理這種平臺差異。我們通過(guò)#if UNITY_UV_STARTS_AT_TOP
就可以判斷是否是DX系列平臺,正常的OpenGL從下到上的紋素為正,但是到DX下改成從下到上,如果主紋理的uv值y方向反了,那么這個(gè)_MainTex_TexelSize.y就小于0。我們通過(guò)這樣一個(gè)判斷語(yǔ)句,就可以自己處理平臺之間的差異了。上面的shader中就是增加了這樣一句判斷,就解決了在PC上出現反向的問(wèn)題。