UnityShader-BilateralFilter(双边滤波,磨皮滤镜)
前言
最近趁著Steam打折入了好多個游戲,昨天剛剛通關了一個《Ruiner》的游戲。
游戲類似《孤膽槍手》,但是加入了很多技能元素和動作元素,加上游戲本身的卡通渲染+賽博朋克風格,總體感覺還是不錯的。
國慶玩了幾個大作連刷了幾天,有點傷。最近反倒傾向于玩一些小游戲,簡單粗暴。不用它三七二十一,莽夫上去就是干!
我發現blog也是這樣,最近半年寫的blog似乎都有點長,有時候也來點短小精悍的換換口味。今天就來玩一個簡單但是又比較好玩的效果-雙邊濾波。
簡介
雙邊濾波(Bilateral Filter),可能沒有高斯濾波那樣著名,但是如果說磨皮濾鏡,那肯定是無人不知無人不曉了,用雙邊濾波就可以實現很好的皮膚濾鏡效果,不管臉上有多少麻子,用完雙邊濾波,瞬間變身白富美。下圖來自一款磨皮濾鏡插件的效果圖,左側為原始效果,右側為濾鏡后的效果。本文中我們也會實現一個雙邊濾波后處理,可以達到近似的效果。
所謂濾波,是將信號中特定波段頻率濾除的操作。正常高斯模糊(高斯濾波)在進行采樣的時候,主要是考慮了像素之間的距離關系(空域信息domain),也就是按照正態分布將當前像素點周圍像素加權平均得到濾波后的結果,可以得到很好的模糊效果。但是高斯模糊是對整個圖像無差異地進行模糊,也就是整張圖片全部模糊掉。關于高斯模糊,之前在Unity Shader后處理:高斯模糊這篇blog中詳細介紹過,這里不再贅述。
高斯模糊的定義如下:
而雙邊濾波是高斯濾波進階版本,可以在模糊的同時保持圖像中的邊緣信息。除了考慮正常高斯濾波的空域信息(domain)外,還要考慮另外的一個圖像本身攜帶的值域信息(range)。這個值域信息的選擇并非唯一的,可以是采樣點間像素顏色的差異,可以是采樣點像素對應的法線信息,可以是采樣點像素對應的深度信息(3D渲染中拿到法線和深度還是要比單純的2D圖像處理可以做的事情多不少哈)。
雙邊濾波定義如下:
可見,除了正常的圖像距離權重c之外,額外添加了圖像相似信息權重s,而s是基于圖像本身信息獲得的,使用c和s相乘的結果作為最終的權重。即在采樣圖像及周圍點時,對于每一個像素點,需要乘以距離權重乘以圖片相似性權重相加得到總和,然后除以每一個像素點距離權重乘以相似性權重的和,即:
關于雙邊濾波對圖像進行處理,可以參考《Bilateral Filtering for Gray and Color Images》這篇論文(似乎要出墻),上文高斯濾波定義,雙邊濾波定義公式均來自該論文。
基于顏色差值的雙邊濾波
先來看一下基于顏色差值的雙邊濾波,這是圖像處理方面最常用的濾波方式,也是傳說中的磨皮濾鏡的實現方式。我們的值域信息權重來源于圖像本身,也就是采樣圖像當前像素點,然后對于其周圍的像素點,計算周圍像素點與當前像素點顏色(轉為灰度)后的差值作為權重進行雙邊濾波操作。
此處本人使用了后處理進行雙邊濾波操作,由于高斯濾波和雙邊濾波操作本身屬于線性操作,可以拆分成橫向縱向兩個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);}ENDCGSubShader{Tags { "RenderType"="Opaque" }LOD 100//Pass 0 Gaussian BlurPass{CGPROGRAM#pragma vertex vert#pragma fragment frag_gaussianENDCG}Pass 1 BilateralFiter Blur ColorPass{CGPROGRAM#pragma vertex vert#pragma fragment frag_bilateralcolorENDCG}Pass 2 BilateralFiter Blur NormalPass{CGPROGRAM#pragma vertex vert#pragma fragment frag_bilateralnormalENDCG}} }C#代碼如下:
/********************************************************************FileName: BilateralFilterEffect.csDescription: 高斯濾波,雙邊濾波(基于顏色差值,基于法線)history: 15:10:2018 by puppet_masterhttps://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啦!
總結
以上是生活随笔為你收集整理的UnityShader-BilateralFilter(双边滤波,磨皮滤镜)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Photoshop+Lightroom
- 下一篇: 徐进的信念:IE工业工程与精益生产管理的