最近趁着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);
}
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();
}
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啦!