一.什么是屏幕后处理
1.1.概念
屏幕后处理是指渲染完整个场景得到屏幕图像后,再对屏幕图像做处理,实现屏幕特效。使用这种技术, 可以为游戏画面添加更多的艺术效果, 例如景深( Depth of Field)、 运动模糊( Motion Blur) 等。
实现屏幕后处理效果的关键在于得到渲染后的屏幕图像,Unity提供了对应的接口 OnRenderImage ,其函数声明
MonoBehaviour.OnRenderImage(RenderTexture scr,RenderTexture dest)
参数 src:源纹理,用于存储当前渲染的得到的屏幕图像
**参数 dest **:目标纹理,经过一系列操作后,用于显示到屏幕的图像
在OnRenderImage函数中,通常调用 Graphics.Blit函数 完成对渲染纹理的处理
Graphics.Blit是每帧渲染时执行t, 有3个重载:
public Static void Blit(Texture src,RenderTexture dest)
public Static void Blit(Texture src,RenderTexture dest,Material mat,int pass = -1)
public Static void Blit(Texture src,Material mat,int pass = -1)
参数 mat : 使用的材质,该材质使用的Unity Shader将会进行各种屏幕后处理操作,src对应的纹理会传递给Shader中_MainTex的纹理属性
参数 pass:默认值为-1,表示依次调用Shader内所有Pass,否则调用索引指定的Pass。
第三个重载:Graphics.Blit(原图,目标,材质,指定PASS索引),第四个参数是指定shader中的第几个Pass进行处理,索引是0开始。如:0 就是第一个Pass。 即Shader当前起作用的SubShader的第一个Pass进行渲染,其他Pass都不会执行!所以这就是可以控制每一个Pass的执行顺序;而如果这个值为-1,则会按顺序执行所有Pass。
默认情况下,OnRenderImage 函数会在所有不透明和透明Pass执行完后被调用,若想在不透明Pass执行完后调用,即不对透明物体产生影响,可以在OnRenderImage函数前添加ImageEffectOpaque的属性实现。
Unity中实现屏幕后处理出效果,通常步骤:
1.在摄像机添加屏幕后处理脚本,该脚本中会实现OnRenderImage函数获取当前屏幕渲染纹。
2.调用Graphics.Blit函数使用特定Shader对当前图像进行处理,再将最终目标纹理渲染到屏幕上。对于复杂的后处理特效,需要多次调用Graphics.Blit函数
1.2.具体做法
在进行屏幕后处理前,需要检查是否满足后处理条件,创建一个用于屏幕后处理效果的基类,在实现屏幕后处理效果时,只需继承该基类,再实现派生类中的具体操作,完整代码:
//希望在编辑器状态下也可以执行该脚本来查看效果
[ExecuteInEditMode]
//所有的屏幕后处理效果都需要绑定在某个摄像机上
[RequireComponent(typeof(Camera)]
public class PostEffectsBase : MonoBehaviour {
protected void CheckResources() {
bool isSupported = CheckSupport();
if (isSupported == false) {
NotSupport();
}
}
protected bool CheckSupport() {
if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
return false;
}
return true;
}
protected void NotSupport() {
enabled = false;
}
// Use this for initialization
void Start () {
//检查资源和条件是否支持屏幕后处理
CheckResources();
}
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
if (shader == null) {
return null;
}
if (shader.isSupported && material && material.shader == shader) {
return material;
}
if (!shader.isSupported)
{
return null;
}
else {
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
二.调整亮度、饱和度和对比度
新建一个脚本,名为BrightnessSaturationAndContrast.cs。添加到摄像机上。
using UnityEngine;
using System.Collections;
public class BrightnessSaturationAndContrast : PostEffectsBase {
public Shader briSatConShader;
private Material briSatConMaterial;
public Material material {
get {
briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, briSatConMaterial);
return briSatConMaterial;
}
}
[Range(0.0f, 3.0f)]
public float brightness = 1.0f;
[Range(0.0f, 3.0f)]
public float saturation = 1.0f;
[Range(0.0f, 3.0f)]
public float contrast = 1.0f;
void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_Brightness", brightness);
material.SetFloat("_Saturation", saturation);
material.SetFloat("_Contrast", contrast);
//使用这个函数进行将source传入material的_MainTex(必须保证有这个字段)纹理进行用material的着色器渲染
//注意:它会执行所有Pass,进行渲染,相当于第四个参数为-1
Graphics.Blit(src, dest, material);//第三个参数是material材质,还用它身上的着色器shader进行渲染
} else {
//否则,只是将原色source图像输出到屏幕,实际上是啥都没干。因为没有着色器传入(第三个参数material身上的着色器)
Graphics.Blit(src, dest);
}
}
}
继承自屏幕处理基类PostEffectsBase,指定shader,并根据该shader创建新的材质,通过OnRenderImage方法和Graphics.Blit方法将参数传递到shader中,完成着色,shader代码:
Shader "Custom/Chapter12_BrightnessSaturateAndContrast" {
Properties{
_MainTex("Maintex",2D)="white"{}
_Brightness("Brightness",Float)=1
_Saturation("Saturation",Float)=1
_Contrast("Contrast",Float)=1
//Graphics.Blit(src,dest,material)会将第一个参数传递给Shader中名为_MainTex的属性
}
SubShader{
Pass{
ZTest Always
Cull Off
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _MainTex_ST;
half _Brightness;
half _Saturation;
half _Contrast;
struct v2f{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
};
//appdata_img为Unity内置的结构体,只包含图像处理必须的顶点坐标和纹理坐标
v2f vert(appdata_img v){
v2f o;
o.pos=UnityObjectToClipPos(v.vertex);
o.uv=v.texcoord; //屏幕后处理得到的纹理和要输出的纹理坐标是相同的
return o;
}
//屏幕特效使用的顶点着色器代码通常比较简单,我们只需要进行必须的顶点变换
//更重要的是,我们需要把正确的纹理坐标传递给片元着色器,以便对屏幕图像进行正确的采样
//使用了内置appdata_img 结构体作为顶点着色器的输入
//可以在 UnityCGxginc 中找到该结构体的声明, 它只包含了图像处理时必需的顶点坐标和纹理坐标等变量
fixed4 frag(v2f i):SV_Target{
//采样
fixed4 renderTex = tex2D(_MainTex, i.uv);
//首先原色RGB*亮度值 就拿到了亮度值影响后的颜色
fixed3 finalColor = renderTex.rgb * _Brightness;
//用一些特殊的浮点数进行乘以原图的r,g,b值再相加 得到一个特殊的值
fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
//用这个特殊值构成了一个饱和度为0的颜色
fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
//_Saturation是饱和度,为0时则是饱和度为0的颜色值,否则越接近上面处理后的颜色(亮度影响后的)
finalColor = lerp(luminanceColor, finalColor, _Saturation);
//对比度为0的颜色值
fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
//_Contrast为0时,拿到的是对比度为0的颜色值,否则越接近上面处理后的颜色(亮度影响后+饱和度影响后的)
finalColor = lerp(avgColor, finalColor, _Contrast);
//输出,A通道采用原图A通道值
return fixed4(finalColor, renderTex.a);
}
ENDCG
}
}
FallBack Off
}
Graphics.Blit(src,dest,material)会将第一个参数传递给Shader中名为_MainTex的属性,然后用这个shader进行渲染。
三.边缘检测
屏幕后处理中的边缘检测是利用一些边缘检测算子对图像中的像素进行卷积操作。
3.1.卷积概念
在图像处理中,卷积操作指的就是使用一个卷积和对一张图像中的每个像素进行一系列操作。卷积核通常是一个四方形网格结构,例如2x2,3x3的方形区域,该区域内每个网格都有一个权重值。当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,如下图所示。
具体关于图像处理中的卷积计算,可以参考这篇博客(http://www.cnblogs.com/freeblues/p/5738987.html)
这样的计算过程虽然简单,但可以实现很多常见的图像处理效果,例如图像模糊、边缘检测等。
3.2.边缘检测算子
卷积操作的神奇指出 在于选择的卷积核,用于边缘检测的卷积核(边缘检测算子)是什么? 首先想一下 如果相邻像素之间存在差别明显的颜色,亮度等属性,那么他们之间应该有一条边界。这种相邻像素之间的差值可以用梯度来表示,边缘处的梯度绝对值比较大,所以,就出现下面几种边缘检测算子:
边缘检测算子包含两个方向的卷积核,用来计算水平方向和竖直方向的梯度值,得到两个方向的梯度值,而整体的梯度值可以是两个方向上的梯度值平方和开根,为了节约性能可以是两个方向梯度值的绝对值求和,整体梯度的值越大,说明该像素点则越有可能是边缘位置。
关于相关算子的介绍,可以查看下边的连接
https://wenku.baidu.com/view/abe192dc28ea81c758f5786b.html?from=search
3.3.实现
使用Sobel算子进行边缘检测,实现描边效果。
新建一个脚本,名为EdgeDetectiont.cs。添加到摄像机上。
using UnityEngine;
using System.Collections;
public class EdgeDetection : PostEffectsBase {
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f;//当edgesOnly为0时,边缘将会叠加在原渲染图像上,
//为1时,则会只显示边缘,不显示原渲染图像
public Color edgeColor = Color.black;//边缘颜色
public Color backgroundColor = Color.white;//背景颜色
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
新建一个Unity Shader。
Shader "Unlit/Chapter12-MyEdgeDetection"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_EdgeOnly ("Edge Only", Float) = 1.0
_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
_BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
}
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment fragSobel
sampler2D _MainTex;
//xxx_TexelSize 是Unity为我们提供访问xxx纹理对应的每个纹素的大小。
//例如一张512×512的纹理,该值大小为0.001953(即1/512)。由于卷积需要对相邻区域内的纹理
//进行采样,因此我们需要它来计算相邻区域的纹理坐标
uniform half4 _MainTex_TexelSize;
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0;
};
v2f vert(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
//我们在v2f结构体中定义了一个维数为9的纹理数组,对应了使用Sobel算子采样时需要的9个
//邻域纹理坐标。通过把计算采样纹理坐标的代码从片元着色器转移到顶点着色器中,可以减少
//运算,提供性能。由于从顶点着色器到片元着色器的插值是线性的,因此这样的转移不会影响
//纹理坐标的计算结果。
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
//利用Sobel算子计算梯度值
half Sobel(v2f i) {
//水平方向卷积核
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
//竖直方向卷积核
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) {
//采样,得到亮度值
texColor = luminance(tex2D(_MainTex, i.uv[it]));
//水平方向上梯度
edgeX += texColor * Gx[it];
//竖直方向上梯度
edgeY += texColor * Gy[it];
}
//edge 越小,表面该位置越可能是一个边缘点。
half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i);
//如果有看Sobel解释的同学,可以看看这里的注释)
//1. 为什么 1 - 总体梯度值呢? 因为当 当前颜色 和 周围相差越大时,梯度越大, 此时edge 越小,lerp插值得到的是边缘颜色!
// 如果,你将lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);改为 lerp(tex2D(_MainTex, i.uv[4], _EdgeColor), edge);
// 此时,edge 就等于 梯度值, 梯度值越大,说明当前像素点颜色纸和周围颜色相差越大,说明是边缘!因此显示出边缘颜色!!
// i.uv[4]是当前像素点纹理坐标
//withEdgeColor:当越接近边缘时,输出的是边缘颜色,否则输出的是原图颜色,中间是这2个颜色的插值结果,以离边缘的远近程度进行插值运算
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
//onlyEdgeColor:当离边缘越近时,输出边缘颜色,否则输出背景纯色颜色,中间是这2个颜色的插值结果,,以离边缘的远近程度进行插值运算
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
//将上方2个颜色进行插值_EdgeOnly,当为1时,则只显示出边缘颜色+背景纯色颜色(注意背景是自定义的纯色图,而不是屏幕背景哦)
//否则为0时,则是显示出边缘+原图
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
}
}
FallBack Off
}
四.高斯模糊
模糊实现的方式有多种,有均值模糊,中值模糊。
从数字信号处理的角度看,图像模糊的本质一个过滤高频信号,保留低频信号的过程。过滤高频的信号的一个常见可选方法是卷积滤波。从这个角度来说,图像的高斯模糊过程即图像与正态分布做卷积。由于正态分布又叫作“高斯分布”,所以这项技术就叫作高斯模糊。而由于高斯函数的傅立叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波器。
用于高斯模糊的高斯核(Gaussian Kernel)是一个正方形的像素阵列,其中像素值对应于2D高斯曲线的值。其中σ 是标准方差(一般取值为1),x和y分别对应了当前位置到卷积核中心的整数距离。
要构建一个高斯核,只需要计算高斯核中各个位置对应的高斯值。为了保证滤波后的图像不会变暗,需要对高斯核中的权重进行归一化,即让每个权重除以所有权重的和,这样可以保证所有权重的和为1。因此,高斯函数中e的前面的系数实际不会对结果又任何影响。
高斯核的维数越高,处理后的图像模糊程度会越高。当使用一个NxN的高斯核对一个WxH的图像进行处理,那么对像素值的采样结果为NxNxWxH次,N越大采样的次数就越大。二维的高斯核可以拆成两个一维的高斯核,得到的结果和直接使用二维高斯核的结果是一样的,这样可以使采样次数降低到2xNxWxH次。而两个一维的高斯核中权重值有重复的权重值,例如一个5x5的一维高斯核只需要记录三个权重值。
我们将会使用上述5×5的高斯核对原图像进行高斯模糊。我们将先后调用两个Pass,第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass再使用水平方向的一维高斯核对图像进行滤波,得到最红的目标图像。在实现中,还将利用图像缩放来进一步提高性能,并通过调整高斯滤波的应用次数来控制模糊程度。
新建一个脚本,名为GaussianBlur.cs。添加到摄像机上。
public class GaussianBlur : PostEffectsBase{
public Shader gaussianBlurShader;
private Material gaussianBlurMaterial;
public Material material{
get{
gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader,gaussianBlurMaterial);
return gaussianBlurMaterial;
}
}
//迭代次数
[Range(0,4)]
public int iterations = 3;
//模糊范围
[Range(0.2f,3.0f)]
public float blurSpread = 0.6f;
//缩放系数
[Range(1, 8)]
public int downSample = 2;
//第一个版本,最简单的处理
/*void OnRenderImage(RenderTexture src,RenderTexture dest){
if(material == null){
int rtW = src.width;
int rtH = src.height;
//分配一个缓冲区
RenderTexture buffer = RenderTexture.GetTemporary(rtW,rtH,0);
Graphics.Blit(src,buffer,material,0);
Graphics.Blit(buffer,dest,material,1);
RenderTexture.ReleaseTemporary(buffer);
}
else{
Graphics.Blit(src,dest);
}
} */
//第二个版本 ,增加降采样的处理
/*void OnRenderImage(RenderTexture src,RenderTexture dest){
if(material == null){
//使用了小于原屏幕分辨率的尺寸
int rtW = src.width/downSample;
int rtH = src.height/downSample;
//分配一个缓冲区
RenderTexture buffer = RenderTexture.GetTemporary(rtW,rtH,0);
//临时渲染纹理的滤波模式设置为双线性
buffer.filterMode = FilterMode.Bilinear;
Graphics.Blit(src,buffer,material,0);
Graphics.Blit(buffer,dest,material,1);
RenderTexture.ReleaseTemporary(buffer);
}
else{
Graphics.Blit(src,dest);
}
} */
//第三个版本,增加降采样处理及迭代的影响
void OnRenderImage(RenderTexture src,RenderTexture dest){
if(material == null){
//使用了小于原屏幕分辨率的尺寸
int rtW = src.width/downSample;
int rtH = src.height/downSample;
//分配一个缓冲区
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW,rtH,0);
//临时渲染纹理的滤波模式设置为双线性
buffer.filterMode = FilterMode.Bilinear;
Graphics.Blit(src,buffer0);
//进行迭代模糊
for(int i=0;i<iterations;i++){
material.SetFloat("_BlurSoze",1.0f+i*blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);
Graphics.Blit(buffer0,buffer1,material,0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW,rtH,0);
Graphics.Blit(buffer0,buffer1,material,1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
Graphics.Blit(buffer0,dest);
RenderTexture.ReleaseTemporary(buffer0);
}
else{
Graphics.Blit(src,dest);
}
}
}
新建一个Unity Shader。
Shader "Unlit/Chapter12-MyGaussianBlur"
{
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
//在SubShader 块中利用CGINCLUDE 和 ENDCG 来定义一系列代码
//这些代码不需要包含在Pass语义块中,在使用时,我们只需要在Pass中指定需要
//使用的顶点着色器和片元着色器函数名即可。
//使用CGINCLUDE 来管理代码 可以避免我们编写两个完全一样的frag函数
//这里相当于只是定义 执行还是在下边的Pass中
//使用时,在Pass中直接指定需要使用的着色器函数,避免编写完一样的片元着色器函数
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
v2f vertBlurHorizontal(appdata_img v) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
fixed4 fragBlur(v2f i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
实例效果:五.运动模糊
运动模糊是真实世界中的摄像机的一种效果。如果摄像机在曝光时,场景发生变化,就会产生模糊的画面。计算机产生的图像由于不存在曝光,渲染出来的图像往往是线条清晰,缺少运动模糊。
运动模糊的实现有多种方式
1.利用积累缓存混合多张连续图片
当物体移动产生多张图片后,取这些图片的平均值作为最后的运动模糊图像。这种方式对性能消耗有较大影响,获取多张帧图像需要在同一帧内多次进行场景渲染。
2.使用速度缓存
这个缓存中存储各个像素的运动速度,使用该值决定模糊的方向和大小。
这里使用类似上述第一种方法的实现来模拟运动模糊的效果。我们不需要再一帧中把场景渲染多次,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法与原始的利用累计缓存的方法相比性能更好,但模糊效果可能会略有影响。
实例代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MotionBlur : PostEffectsBase{
public Shader motionBlurShader;
private Material motionBlurMaterial = null;
public Material material
{
get
{
motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
return motionBlurMaterial;
}
}
[Range(0f, 0.9f)]
public float blurAmount = 0.5f;
//blurAmount 的值越大, 运动拖尾的效果就越明显, 为了防止拖尾效果完全替代当前帧的渲染
//结果, 我们把它的值截取在 0.0-0.9 范围内。
private RenderTexture accumulationTexture;
private void OnDisable()
{
DestroyImmediate(accumulationTexture);//销毁临时纹理
}
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
//确保临时渲染纹理得大小是和屏幕一样得,注意:这个临时渲染纹理是不需要清空的,只有在禁用掉组件时才会销毁
if(accumulationTexture == null || accumulationTexture.width != source.width ||
accumulationTexture.height != source.height)
{
DestroyImmediate(accumulationTexture);
accumulationTexture = new RenderTexture(source.width, source.height, 0);
//不保存和不显示在Hierarchy中
accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
//将屏幕图像渲染到临时纹理上
Graphics.Blit(source, accumulationTexture);
}
//进行一个渲染纹理的恢复操作:恢复操作发生在渲染到纹理而该纹理又没有被提前清空或销毁的前提下!这个操作就是我在shader注释所说的
//进行一个恢复操作,不然上一帧的颜色会一直逗留在屏幕中 你可以试试注释掉行代码
accumulationTexture.MarkRestoreExpected();
//当blurAmount越大 传入的值越小,运动模糊效果越显著,因为上一帧的颜色被完全保留下来了,
//具体看shader代码有详细解释。。。
material.SetFloat("_BlurAmount", 1.0f - blurAmount);
//将渲染的结果叠加到临时纹理,注意,还没叠加之前临时纹理还保留着上一帧的渲染结果,然后叠加是指Shader的一个透明度混合操作进行的
//shader代码用了2个PASS,第一个PASS是只混合RGB通道,A通道不会进行写入到缓冲区,第二个PASS是将原始(最开始的)那个A通道,写入到缓冲区
Graphics.Blit(source, accumulationTexture, material);
//临时纹理输出到屏幕
Graphics.Blit(accumulationTexture, destination);
}
else
{
Graphics.Blit(source, destination);
}
}
}
新建一个Unity Shader。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "MilkShader/Twently/M_MotionBlur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BlurAmount ("Blur Amount", Float) = 1.0
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed _BlurAmount;
struct v2f{
float4 pos : SV_POSITION;
half2 uv : TEXCOORD0;
};
v2f vert(appdata_img v){
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 fragRGB(v2f i) : SV_Target{
//使用_BlurAmount作为这个片元的输出颜色A通道,在第一个PASS中 我们开启了混合,_BlurAmount就是作为混合因子的
//即 SrcAlpha 为 _BlurAmount
// OneMinusSrcAlpha = 1 - _BlurAmount
// outcolor = sourcecolor * SrcAlpha + destiColor * OneMinusSrcAlpha = sourcecolo * _BlurAmount + destiColor * (1-_BlurAmount)
//当_BlurAmount越小时,完全保留颜色缓冲区的颜色值,也就是上一帧(A帧)的屏幕颜色被完全保留下来,当然帧(B帧)的颜色会在下一帧进行输出到屏幕。
//在下一帧(C帧)时的时,UNITY会清空当前帧的上一帧(A帧)的屏幕颜色值或衰减颜色值等处理
//所以_BlurAmount越小时,越能感觉到运动模糊效果,否则上一帧的颜色纸得不到保留,那就完全没有运动模糊效果
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}
half4 fragA(v2f i): SV_Target {
return tex2D(_MainTex, i.uv);
}
ENDCG
ZTest Always Cull Off ZWrite Off
//这里将RGB和A通道分开,是由于在做混合时,按照_BlurAmount参数值将源 图像和目标图像进行混合
//而同时不让其纹理受到A通道值的影响,只是用来做混合,不改变其透明度
Pass
{
Blend SrcAlpha OneMinusSrcAlpha
//只输出RGB到屏幕上
ColorMask RGB
CGPROGRAM
#pragma vertex vert
#pragma fragment fragRGB
ENDCG
}
Pass
{
Blend One Zero
//只输出A到屏幕上
ColorMask A
CGPROGRAM
#pragma vertex vert
#pragma fragment fragA
ENDCG
}
}
Fallback Off
}