1. 程式人生 > >UnityShader-BilateralFilter(雙邊濾波,磨皮濾鏡)

UnityShader-BilateralFilter(雙邊濾波,磨皮濾鏡)

前言

最近趁著Steam打折入了好多個遊戲,昨天剛剛通關了一個《Ruiner》的遊戲。

遊戲類似《孤膽槍手》,但是加入了很多技能元素和動作元素,加上游戲本身的卡通渲染+賽博朋克風格,總體感覺還是不錯的。

國慶玩了幾個大作連刷了幾天,有點傷。最近反倒傾向於玩一些小遊戲,簡單粗暴。不用它三七二十一,莽夫上去就是幹!

我發現blog也是這樣,最近半年寫的blog似乎都有點長,有時候也來點短小精悍的換換口味。今天就來玩一個簡單但是又比較好玩的效果-雙邊濾波。

簡介

雙邊濾波(Bilateral Filter),可能沒有高斯濾波那樣著名,但是如果說磨皮濾鏡,那肯定是無人不知無人不曉了,用雙邊濾波就可以實現很好的面板濾鏡效果,不管臉上有多少麻子,用完雙邊濾波,瞬間變身白富美。下圖來自一款

磨皮濾鏡外掛的效果圖,左側為原始效果,右側為濾鏡後的效果。本文中我們也會實現一個雙邊濾波後處理,可以達到近似的效果。

所謂濾波,是將訊號中特定波段頻率濾除的操作。正常高斯模糊(高斯濾波)在進行取樣的時候,主要是考慮了畫素之間的距離關係(空域資訊domain),也就是按照正態分佈將當前畫素點周圍畫素加權平均得到濾波後的結果,可以得到很好的模糊效果。但是高斯模糊是對整個影象無差異地進行模糊,也就是整張圖片全部模糊掉。關於高斯模糊,之前在Unity Shader後處理:高斯模糊這篇blog中詳細介紹過,這裡不再贅述。

高斯模糊的定義如下:

而雙邊濾波是高斯濾波進階版本,可以在模糊的同時保持影象中的邊緣資訊。除了考慮正常高斯濾波的空域資訊(domain)外,還要考慮另外的一個影象本身攜帶的值域資訊(range)。這個值域資訊的選擇並非唯一的,可以是取樣點間畫素顏色的差異,可以是取樣點畫素對應的法線資訊,可以是取樣點畫素對應的深度資訊(3D渲染中拿到法線和深度還是要比單純的2D影象處理可以做的事情多不少哈)。

雙邊濾波定義如下:

可見,除了正常的影象距離權重c之外,額外添加了影象相似資訊權重s,而s是基於影象本身資訊獲得的,使用c和s相乘的結果作為最終的權重。即在取樣影象及周圍點時,對於每一個畫素點,需要乘以距離權重乘以圖片相似性權重相加得到總和,然後除以每一個畫素點距離權重乘以相似性權重的和,即:

基於顏色差值的雙邊濾波

先來看一下基於顏色差值的雙邊濾波,這是影象處理方面最常用的濾波方式,也是傳說中的磨皮濾鏡的實現方式。我們的值域資訊權重來源於影象本身,也就是取樣影象當前畫素點,然後對於其周圍的畫素點,計算周圍畫素點與當前畫素點顏色(轉為灰度)後的差值作為權重進行雙邊濾波操作。

此處本人使用了後處理進行雙邊濾波操作,由於高斯濾波和雙邊濾波操作本身屬於線性操作,可以拆分成橫向縱向兩個Pass進行,大大計算的時間複雜度。對於高斯模糊的正態分佈函式,對於影象處理可以按照正態分佈公式動態生成,不過在遊戲這種效能吃緊的後處理中,直接使用預計算好的正態分佈值即可。

Shader關鍵程式碼如下:

half CompareColor(fixed4 col1, fixed4 col2)
{
	float l1 = LinearRgbToLuminance(col1.rgb);
	float l2 = LinearRgbToLuminance(col2.rgb);
	return smoothstep(_BilaterFilterFactor, 1.0, 1.0 - abs(l1 - l2));
}

fixed4 frag_bilateralcolor (v2f i) : SV_Target
{
	float2 delta = _MainTex_TexelSize.xy * _BlurRadius.xy;
	fixed4 col = tex2D(_MainTex, i.uv);
	fixed4 col0a = tex2D(_MainTex, i.uv - delta);
	fixed4 col0b = tex2D(_MainTex, i.uv + delta);
	fixed4 col1a = tex2D(_MainTex, i.uv - 2.0 * delta);
	fixed4 col1b = tex2D(_MainTex, i.uv + 2.0 * delta);
	fixed4 col2a = tex2D(_MainTex, i.uv - 3.0 * delta);
	fixed4 col2b = tex2D(_MainTex, i.uv + 3.0 * delta);
	
	half w = 0.37004405286;
	half w0a = CompareColor(col, col0a) * 0.31718061674;
	half w0b = CompareColor(col, col0b) * 0.31718061674;
	half w1a = CompareColor(col, col1a) * 0.19823788546;
	half w1b = CompareColor(col, col1b) * 0.19823788546;
	half w2a = CompareColor(col, col2a) * 0.11453744493;
	half w2b = CompareColor(col, col2b) * 0.11453744493;
	
	half3 result;
	result = w * col.rgb;
	result += w0a * col0a.rgb;
	result += w0b * col0b.rgb;
	result += w1a * col1a.rgb;
	result += w1b * col1b.rgb;
	result += w2a * col2a.rgb;
	result += w2b * col2b.rgb;
	
	result /= w + w0a + w0b + w1a + w1b + w2a + w2b;

	return fixed4(result, 1.0);
}

C#關鍵程式碼如下:

private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
    var tempRT = RenderTexture.GetTemporary(source.width, source.height, 0, source.format);
    var blurPass = (int)blurType;
    filterMaterial.SetFloat("_BilaterFilterFactor", 1.0f - bilaterFilterStrength);

    filterMaterial.SetVector("_BlurRadius", new Vector4(BlurRadius, 0, 0, 0));
    Graphics.Blit(source, tempRT, filterMaterial, blurPass);

    filterMaterial.SetVector("_BlurRadius", new Vector4(0, BlurRadius, 0, 0));
    Graphics.Blit(tempRT, destination, filterMaterial, blurPass);

    RenderTexture.ReleaseTemporary(tempRT);
}

我們把上圖的麻子臉妹紙放到場景中的一個片上,原始的照片效果如下,可見面板上還是有一些瑕疵的:

使用普通的高斯濾波效果如下,整個影象都模糊了,如果濾鏡做成這樣,肯定要被打死的:

再看一下基於顏色差值的雙邊濾波效果,去除了臉上的瑕疵的同時,還保持了細節效果,磨皮效果棒棒噠:

基於法線的雙邊濾波

下面才是我寫這篇blog的出發點,畢竟我不是搞影象處理的,2333。對於3D渲染的場景,我們除了可以得到當前螢幕上顯示的影象之外,還可以得到對應的全螢幕的深度值,全螢幕的法線值。使用深度或者法線的差異作為雙邊濾波的值域資訊,可以讓我們對3D場景結果濾波時保證邊界拐角的地方不被模糊,保持邊緣。

我們將上面的Shader稍加修改,這裡我們使用了前向渲染開啟了CameraDepthNormalTexture,可以得到全場景法線圖,然後我們對於每個取樣點的權重使用當前畫素點法線和周圍取樣點的法線差異作為權重,直接使用向量點乘表示兩個向量的共執行緒度即可。

float3 GetNormal(float2 uv)
{
	float4 cdn = tex2D(_CameraDepthNormalsTexture, uv);
	return DecodeViewNormalStereo(cdn);
}

half CompareNormal(float3 normal1, float3 normal2)
{
	return smoothstep(_BilaterFilterFactor, 1.0, dot(normal1, normal2));
}

fixed4 frag_bilateralnormal (v2f i) : SV_Target
{
	float2 delta = _MainTex_TexelSize.xy * _BlurRadius.xy;
	
	float2 uv = i.uv;
	float2 uv0a = i.uv - delta;
	float2 uv0b = i.uv + delta;	
	float2 uv1a = i.uv - 2.0 * delta;
	float2 uv1b = i.uv + 2.0 * delta;
	float2 uv2a = i.uv - 3.0 * delta;
	float2 uv2b = i.uv + 3.0 * delta;
	
	float3 normal = GetNormal(uv);
	float3 normal0a = GetNormal(uv0a);
	float3 normal0b = GetNormal(uv0b);
	float3 normal1a = GetNormal(uv1a);
	float3 normal1b = GetNormal(uv1b);
	float3 normal2a = GetNormal(uv2a);
	float3 normal2b = GetNormal(uv2b);
	
	fixed4 col = tex2D(_MainTex, uv);
	fixed4 col0a = tex2D(_MainTex, uv0a);
	fixed4 col0b = tex2D(_MainTex, uv0b);
	fixed4 col1a = tex2D(_MainTex, uv1a);
	fixed4 col1b = tex2D(_MainTex, uv1b);
	fixed4 col2a = tex2D(_MainTex, uv2a);
	fixed4 col2b = tex2D(_MainTex, uv2b);
	
	half w = 0.37004405286;
	half w0a = CompareNormal(normal, normal0a) * 0.31718061674;
	half w0b = CompareNormal(normal, normal0b) * 0.31718061674;
	half w1a = CompareNormal(normal, normal1a) * 0.19823788546;
	half w1b = CompareNormal(normal, normal1b) * 0.19823788546;
	half w2a = CompareNormal(normal, normal2a) * 0.11453744493;
	half w2b = CompareNormal(normal, normal2b) * 0.11453744493;
	
	half3 result;
	result = w * col.rgb;
	result += w0a * col0a.rgb;
	result += w0b * col0b.rgb;
	result += w1a * col1a.rgb;
	result += w1b * col1b.rgb;
	result += w2a * col2a.rgb;
	result += w2b * col2b.rgb;
	
	result /= w + w0a + w0b + w1a + w1b + w2a + w2b;
	return fixed4(result, 1.0);
}

我們使用一個3D場景,原始效果如下:

高斯濾波效果如下,全糊啦!!!

基於法線的雙邊濾波效果如下,還能夠保持場景的邊界效果,僅僅在同一平面內進行模糊:

高斯濾波與兩種雙邊濾波原始碼

把高斯濾波,基於顏色的雙邊濾波和基於法線的雙邊濾波分別作為一個Pass,使用一個後處理效果整合。Shader程式碼如下:

//puppet_master
//https://blog.csdn.net/puppet_master  
//2018.10.15  
//雙邊濾波效果Shader
Shader "AO/BilateralFilterEffect"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	
	CGINCLUDE
	#include "UnityCG.cginc"
	
	struct appdata
	{
		float4 vertex : POSITION;
		float2 uv : TEXCOORD0;
	};
	
	struct v2f
	{
		float2 uv : TEXCOORD0;
		float4 vertex : SV_POSITION;
	};
	
	sampler2D _MainTex;
	float4 _MainTex_ST;
	float4 _MainTex_TexelSize;
	float4 _BlurRadius;
	float _BilaterFilterFactor;
	sampler2D _CameraDepthNormalsTexture;
	
	v2f vert (appdata v)
	{
		v2f o;
		o.vertex = UnityObjectToClipPos(v.vertex);
		o.uv = v.uv;
		return o;
	}
	
	fixed4 frag_gaussian (v2f i) : SV_Target
	{
		float2 delta = _MainTex_TexelSize.xy * _BlurRadius.xy;
		fixed4 col = 0.37004405286 * tex2D(_MainTex, i.uv);
		col += 0.31718061674 * tex2D(_MainTex, i.uv - delta);
		col += 0.31718061674 * tex2D(_MainTex, i.uv + delta);
		col += 0.19823788546 * tex2D(_MainTex, i.uv - 2.0 * delta);
		col += 0.19823788546 * tex2D(_MainTex, i.uv + 2.0 * delta);
		col += 0.11453744493 * tex2D(_MainTex, i.uv - 3.0 * delta);
		col += 0.11453744493 * tex2D(_MainTex, i.uv + 3.0 * delta);
		
		col /= 0.37004405286 + 0.31718061674 + 0.31718061674 + 0.19823788546 + 0.19823788546 + 0.11453744493 + 0.11453744493;
		
		return fixed4(col.rgb, 1.0);
	}
	
	half CompareColor(fixed4 col1, fixed4 col2)
	{
		float l1 = LinearRgbToLuminance(col1.rgb);
		float l2 = LinearRgbToLuminance(col2.rgb);
		return smoothstep(_BilaterFilterFactor, 1.0, 1.0 - abs(l1 - l2));
	}
	
	fixed4 frag_bilateralcolor (v2f i) : SV_Target
	{
		float2 delta = _MainTex_TexelSize.xy * _BlurRadius.xy;
		fixed4 col = tex2D(_MainTex, i.uv);
		fixed4 col0a = tex2D(_MainTex, i.uv - delta);
		fixed4 col0b = tex2D(_MainTex, i.uv + delta);
		fixed4 col1a = tex2D(_MainTex, i.uv - 2.0 * delta);
		fixed4 col1b = tex2D(_MainTex, i.uv + 2.0 * delta);
		fixed4 col2a = tex2D(_MainTex, i.uv - 3.0 * delta);
		fixed4 col2b = tex2D(_MainTex, i.uv + 3.0 * delta);
		
		half w = 0.37004405286;
		half w0a = CompareColor(col, col0a) * 0.31718061674;
		half w0b = CompareColor(col, col0b) * 0.31718061674;
		half w1a = CompareColor(col, col1a) * 0.19823788546;
		half w1b = CompareColor(col, col1b) * 0.19823788546;
		half w2a = CompareColor(col, col2a) * 0.11453744493;
		half w2b = CompareColor(col, col2b) * 0.11453744493;
		
		half3 result;
		result = w * col.rgb;
		result += w0a * col0a.rgb;
		result += w0b * col0b.rgb;
		result += w1a * col1a.rgb;
		result += w1b * col1b.rgb;
		result += w2a * col2a.rgb;
		result += w2b * col2b.rgb;
		
		result /= w + w0a + w0b + w1a + w1b + w2a + w2b;
		return fixed4(result, 1.0);
	}
	float3 GetNormal(float2 uv)
	{
		float4 cdn = tex2D(_CameraDepthNormalsTexture, uv);
		return DecodeViewNormalStereo(cdn);
	}

	half CompareNormal(float3 normal1, float3 normal2)
	{
		return smoothstep(_BilaterFilterFactor, 1.0, dot(normal1, normal2));
	}
	
	fixed4 frag_bilateralnormal (v2f i) : SV_Target
	{
		float2 delta = _MainTex_TexelSize.xy * _BlurRadius.xy;
		
		float2 uv = i.uv;
		float2 uv0a = i.uv - delta;
		float2 uv0b = i.uv + delta;	
		float2 uv1a = i.uv - 2.0 * delta;
		float2 uv1b = i.uv + 2.0 * delta;
		float2 uv2a = i.uv - 3.0 * delta;
		float2 uv2b = i.uv + 3.0 * delta;
		
		float3 normal = GetNormal(uv);
		float3 normal0a = GetNormal(uv0a);
		float3 normal0b = GetNormal(uv0b);
		float3 normal1a = GetNormal(uv1a);
		float3 normal1b = GetNormal(uv1b);
		float3 normal2a = GetNormal(uv2a);
		float3 normal2b = GetNormal(uv2b);
		
		fixed4 col = tex2D(_MainTex, uv);
		fixed4 col0a = tex2D(_MainTex, uv0a);
		fixed4 col0b = tex2D(_MainTex, uv0b);
		fixed4 col1a = tex2D(_MainTex, uv1a);
		fixed4 col1b = tex2D(_MainTex, uv1b);
		fixed4 col2a = tex2D(_MainTex, uv2a);
		fixed4 col2b = tex2D(_MainTex, uv2b);
		
		half w = 0.37004405286;
		half w0a = CompareNormal(normal, normal0a) * 0.31718061674;
		half w0b = CompareNormal(normal, normal0b) * 0.31718061674;
		half w1a = CompareNormal(normal, normal1a) * 0.19823788546;
		half w1b = CompareNormal(normal, normal1b) * 0.19823788546;
		half w2a = CompareNormal(normal, normal2a) * 0.11453744493;
		half w2b = CompareNormal(normal, normal2b) * 0.11453744493;
		
		half3 result;
		result = w * col.rgb;
		result += w0a * col0a.rgb;
		result += w0b * col0b.rgb;
		result += w1a * col1a.rgb;
		result += w1b * col1b.rgb;
		result += w2a * col2a.rgb;
		result += w2b * col2b.rgb;
		
		result /= w + w0a + w0b + w1a + w1b + w2a + w2b;
		return fixed4(result, 1.0);
	}

    ENDCG
	
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		//Pass 0 Gaussian Blur
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag_gaussian
			
			
			ENDCG
		}
		
		////Pass 1 BilateralFiter Blur Color
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag_bilateralcolor
			
			
			ENDCG
		}
		
		////Pass 2 BilateralFiter Blur Normal
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag_bilateralnormal
			
			
			ENDCG
		}
	}
}

C#程式碼如下:

/********************************************************************
 FileName: BilateralFilterEffect.cs
 Description: 高斯濾波,雙邊濾波(基於顏色差值,基於法線)
 history: 15:10:2018 by puppet_master
 https://blog.csdn.net/puppet_master
*********************************************************************/
using UnityEngine;

[ExecuteInEditMode]
public class BilateralFilterEffect : MonoBehaviour
{
    public enum BlurType
    {
        GaussianBlur = 0,
        BilateralColorFilter = 1,
        BilateralNormalFilter = 2,
    }

    private Material filterMaterial = null;
    private Camera currentCamera = null;

    [Range(1,4)]
    public int BlurRadius = 1;
    public BlurType blurType = BlurType.GaussianBlur;
    [Range(0, 0.2f)]
    public float bilaterFilterStrength = 0.15f;

    private void Awake()
    {
        var shader = Shader.Find("AO/BilateralFilterEffect");
        filterMaterial = new Material(shader);
        currentCamera = GetComponent<Camera>();
    }

    private void OnEnable()
    {
        currentCamera.depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    private void OnDisable()
    {
        currentCamera.depthTextureMode &= ~DepthTextureMode.DepthNormals;
    }

    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        var tempRT = RenderTexture.GetTemporary(source.width, source.height, 0, source.format);
        var blurPass = (int)blurType;
        filterMaterial.SetFloat("_BilaterFilterFactor", 1.0f - bilaterFilterStrength);

        filterMaterial.SetVector("_BlurRadius", new Vector4(BlurRadius, 0, 0, 0));
        Graphics.Blit(source, tempRT, filterMaterial, blurPass);

        filterMaterial.SetVector("_BlurRadius", new Vector4(0, BlurRadius, 0, 0));
        Graphics.Blit(tempRT, destination, filterMaterial, blurPass);

        RenderTexture.ReleaseTemporary(tempRT);
    }
}

雙邊濾波在渲染中的作用

上面我們看到了雙邊濾波在影象處理方面的作用超級大,而在渲染中,雙邊濾波也是很有用的一種降噪手段,比高斯濾波要好很多。在很多高階效果,尤其是RayMarching效果中經常需要使用隨機噪聲來降低計算消耗,但是隨之而來的就是會造成結果中包含很多高頻噪聲,最終的結果就需要使用濾波進行降噪。之前本人在RayMarching體積光效果螢幕空間反射效果中都使用了高斯模糊進行降噪,體積光本身就是模糊的,使用高斯模糊或者雙邊濾波本身差異不是很大。螢幕空間反射就可以考慮使用雙邊濾波進行降噪以達到更清晰的反射效果。不過有時候反射本身就需要糊一點才好看哈。

另一個非常重要的需要使用雙邊濾波的效果就是SSAO,即螢幕空間環境光遮蔽效果,使用蒙特卡洛積分得到的效果,隨機取樣數量有限,效果很差,沒有去噪的效果SSAO效果如下(僅顯示AO遮蔽效果):

使用基於法線的雙邊濾波去噪之後的SSAO效果,差別還是灰常大滴:

總結

本blog主要實現了一下雙邊濾波效果,實現了高斯濾波,基於顏色的雙邊濾波,基於法線的雙邊濾波效果。使用雙邊濾波可以在保證影象邊緣的情況下達到去噪的目的,可以很容易地實現影象處理的磨皮濾鏡,實現Dither RayMarching,SSAO等使用隨機取樣的渲染效果的去噪。

本打算寫一個SSAO的blog,然而寫到一半發現雙邊濾波效果還是挺好玩的,正好又通關了一個小遊戲《Runiner》,索性就單獨拿出來寫一篇blog啦!