Unity Shader学习记录(19) ——屏幕后处理效果

屏幕后处理,顾名思义,通常指的是在渲染完整个场景得到屏幕图像后,再对这个图像进行系列操作,实现各种屏幕特效。使用这种技术,可以为游戏画面添加更多的艺术效果,例如景深(DepthofField)、运动模糊(Motion Blur)等。

建立一个基本的屏幕后处理脚本系统

想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而 Unity 为我们提供了这样一个方便的接口–OnRenderImage函数,nRenderImage函数会在所有 透明和不透明 的Pass执行完后才被调用,即对场景中所有物体产生影响,它的函数声明如下: :

将源渲染纹理(存在参数一:src)进行处理,输出目标渲染纹理(存在参数二: dest)

MonoBehaviour.OnRenderImage (RenderTexture src , RenderTexture dest)

public static void Bilt(Texture src , RenderTexture dest);
public static void Bilt(Texture src , RenderTexture dest , Material mat , int pass =-1);
public static void Bilt(Texture src , Material mat , int pass =-1);

src——源纹理,会被传给屏幕后处理Shader中名为 _MainTex 的纹理属性
dest——目标渲染纹理,若值为null,则直接将结果显示到屏幕
mat——屏幕后处理Shader创建的材质
pass——默认值为-1,表示将会调用所有Pass。否则,只调用给定索引的Pass

要在Unity中实现屏幕后处理效果,过程通常如下:
需要在摄像机添加一个用于屏幕后处理的脚本,该脚本会实现OnRenderImage 函数来获取当前屏幕的渲染纹理。然后,再调用Graphics.Blit 函数使用特定的 Unity Shader 来对当前图像进行处理,再把返回的渲染纹理显示到屏幕上。对一些复杂的屏幕特效,可能需要多次调用Graphics.Blit函数来对上一步的输出结果进行下一步处理。

调整屏幕的亮度、饱和度和对比度

在这里插入图片描述
在场景中使图片填充摄像机整个画面。
在这里插入图片描述
将这个脚本挂在在摄像机上

using UnityEngine;
using System.Collections;

public class BrightnessSaturationAndContrast : PostEffectsBase {  //继承 PostEffectsBase

	public Shader briSatConShader; //声明需要的shader
	private Material briSatConMaterial; //声明需要的材质
	//要指定一个Shader来创建一个用于处理染纹理的材质
	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; //调整对比度参数

	//进行特效处理  参数src对应了源纹理,在屏幕后处理技术中,这个参数通常就是当前屏幕的渲染纹理或上一步处理后得到的渲染纹理。参数dest是目标渲染纹理,如果它的值为null就会直接将结果显示在屏幕上
	void OnRenderImage(RenderTexture src, RenderTexture dest) {
		if (material != null) {
			material.SetFloat("_Brightness", brightness);
			material.SetFloat("_Saturation", saturation);
			material.SetFloat("_Contrast", contrast);

			Graphics.Blit(src, dest, material);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

将shader挂在摄像机的脚本上

Shader "MyShader/Brightness Saturation And Contrast" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_Brightness ("Brightness", Float) = 1
		_Saturation("Saturation", Float) = 1
		_Contrast("Contrast", Float) = 1
	}
	SubShader {
		Pass {  
			//屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片
			ZTest Always Cull Off ZWrite Off   //剔除背面   关闭深度斜土
			
			CGPROGRAM  
			#pragma vertex vert  
			#pragma fragment frag  
			  
			#include "UnityCG.cginc"  
			
			sampler2D _MainTex;  
			half _Brightness;  //亮度
			half _Saturation;  //饱和度
			half _Contrast; //对比度
			  
			struct v2f {
				float4 pos : SV_POSITION;
				half2 uv: TEXCOORD0;
			};
			//appdata_img 为unity内置的顶点输入结构体
			v2f vert(appdata_img v) {
				v2f o;
				
				o.pos = UnityObjectToClipPos(v.vertex);
				
				o.uv = v.texcoord;
						 
				return o;
			}
			// 
			fixed4 frag(v2f i) : SV_Target {
				//原屏幕图像(存储在MainTex中)
				fixed4 renderTex = tex2D(_MainTex, i.uv);  
				  
				// 更改亮度
				fixed3 finalColor = renderTex.rgb * _Brightness;
				
				//参数由颜色转灰度值算法赋予
				fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b; // rgb饱和度为0的颜色值
				fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
				finalColor = lerp(luminanceColor, finalColor, _Saturation);
				
				// 对比度
				fixed3 avgColor = fixed3(0.5, 0.5, 0.5);//对比度为0的颜色值
				finalColor = lerp(avgColor, finalColor, _Contrast);
				
				return fixed4(finalColor, renderTex.a);  
			}  
			  
			ENDCG
		}  
	}
	
	Fallback Off
}

边缘检测

边缘检测是图像处理和计算机视觉中一个常见的问题,目的是标出数字图像中亮度变化明显的点
边缘检测是描边效果的一种实现方法,在本节结束后,我们可以得到类似图12.3中的效果。
边缘(edge)是指一个图像上局部特性的不连续性,图像上如果相邻像素之间存在显著的颜色、亮度、纹理等等属性的差别(突变),我们就会认为这里存在一条边界。从卷积角度理解就是:核经过亮像素点时,中间像素值和周围像素值会有一个突变,我们就用梯度(gradient)去描述这一个数值的突变。
————————————————
版权声明:本文为CSDN博主「九九345」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41835314/article/details/127606113
边缘检测的原理是利用一些边缘检测算子对图像进行卷积(convolution)操作,我们首先来了解什么是卷积。

在图像处理中,卷积操作指的就是使用一个卷积核(kerel)对一张图像中的每个像素进行一系列操作。卷积核通常是一个四方形网格结构(例如2x2、3x3 的方形区域),该区域内每个方格都有一个权重值。当对图像中的某个像素进行卷积时,我们会把卷积核的中心放置于该像素上,如图 12.4所示,翻转核之后再依次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到的结果就是该位置的新像素值。

在这里插入图片描述

常见的边缘检测算子

卷积操作的神奇之处在于选择的卷积核。那么,用于边缘检测的卷积核(也被称为边缘检测算子)应该长什么样呢?在回答这个问题前,我们可以首先回想一下边到底是如何形成的。如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,我们就会认为它们之间应该有一条边界这种相邻像素之间的差值可以用梯度 (gradient)来表示,可以想象得到,边缘处的梯度绝对值会比较大。基于这样的理解,有几种不同的边缘检测算子被先后提出来。
在这里插入图片描述
3种常见的边缘检测算子如图 12.5 所示,它们都包含了两个方向的卷积核,分别用于检测水平方向和竖直方向上的边缘信息。在进行边缘检测时,我们需要对每个像素分别进行一次卷积计算,得到两个方向上的梯度值 Gx和Gy,而整体的梯度可按下面的公式计算而得:
在这里插入图片描述
sobel算子结合了高斯平滑和微分求导,充分考虑了位置对边缘影响——相邻点的距离远近对当前像素点的影响程度,距离越近的影响程度越大,从而可以实现图像锐化并突出边缘轮廓的效果。Sobel算子对噪声较多的图片进行边缘检测的效果更好!对灰度变化不敏感。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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;

	public Color edgeColor = Color.black;
	
	public Color backgroundColor = Color.white;
	// OnRenderImage 特效处理
	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);
		}
	}
}

{
	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
		Pass {  
			ZTest Always Cull Off ZWrite Off
			
			CGPROGRAM
			
			#include "UnityCG.cginc"
			
			#pragma vertex vert  
			#pragma fragment fragSobel
			
			sampler2D _MainTex;  
			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 = UnityObjectToClipPos(v.vertex);
				
				half2 uv = v.texcoord;
				// 纹理数组  采样周围像素
				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函数将利用Sobel算子对原图进行边缘检测。
			half Sobel(v2f i) {
				// 卷积核gx gy
				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};		
				// 代码中依次计算9个方格覆盖的像素(包括当前中心像素)的明度值,分别与自己x和y方向的权重值相乘,
				// 叠加后,得到最终中心像素Gx和Gy的梯度值,接着计算edge值(绝对值代替了开根号,为了节省开销),edge越小(
				// 梯度越大),越有可能是边缘点。
				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];
				}
				// 计算差值 越小越有可能是边缘点
				half edge = 1 - abs(edgeX) - abs(edgeY);
				
				return edge;
			}
			//我们首先调用Sobel函数计算当前像素的梯度值edge,并利用该值分别计算了背景为原图和纯色下的颜色值,然后利用_EdgeOnly在两者之间插值得到最终的像素值。
			fixed4 fragSobel(v2f i) : SV_Target {
				half edge = Sobel(i);
				
				fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge); 目的
				fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);//_EdgeOnly的值控制边缘呈现样式的
				return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
 			}
			
			ENDCG
		} 
	}
	FallBack Off
}

在这里插入图片描述
实现的边缘检测仅仅利用了屏幕额色信息,而在实际应用中,物体的纹理、阴影等信息均会影响边缘检测的结果,使得结果包含许多非预期的描边。

高斯模糊

我们将学习卷积的另一个常见应用一一高斯模糊。模糊的实现有很多方法,例如均值模糊和中值模糊。均值模糊同样使用了卷积操作,它使用的卷积核中的各个元素值都相等,且相加等于,也就是说,卷积后得到的像素值是其邻域内各个像素值的平均值。而中值模糊则是选择邻域内对所有像素排序后的中值替换掉原颜色。一个更高级的模糊方法是高斯模糊.
在这里插入图片描述

高斯滤波

在这里插入图片描述
o是标准方差(一般取值为 1),x和分别对应了当前位置到卷积核中心的整数距离。
在这里插入图片描述
第一个Pass将会使用竖直方向的一维高斯核对图像进行滤波,第二个Pass 再使用水平方向的维高斯核对图像进行滤波,得到最终的目标图像。在实现中,我们还将利用图像缩放来进一步损高性能,并通过调整高斯滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。

using UnityEngine;
using System.Collections;

public class GaussianBlur : PostEffectsBase {
	//定义shader
	public Shader gaussianBlurShader;
	//定义material
	private Material gaussianBlurMaterial = null;
	//赋予材质
	public Material material {  
		get {
			//纹理加shader=材质
			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;    

	///使用迭代来加深模糊程度         RenderTexture为摄像机画面  
	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		// 在U3D中有一种特殊的Texture类型,叫做RenderTexture,它本质上一句话是将一个FrameBufferObjecrt连接到一个server-side的Texture对象
		if (material != null) {
			// 缩小采样贴图分辨率
			int rtW = src.width/downSample;
			int rtH = src.height/downSample;
			// 获取图像缓存
			RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);  //GetTemporary根据传入的宽高比,以高为基准,得到的RenderTexture的显示画面的内容,宽度为高*(宽/高)
			buffer0.filterMode = FilterMode.Bilinear;
			//初始化绘制图像
			Graphics.Blit(src, buffer0);

			//利用两个临时缓存在迭代之间进行交替的过程
			for (int i = 0; i < iterations; i++) {
				//传参给shader       
				material.SetFloat("_BlurSize", 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);

				// 计算水平pass
				Graphics.Blit(buffer0, buffer1, material, 1);
				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
			}

			Graphics.Blit(buffer0, dest);
			RenderTexture.ReleaseTemporary(buffer0);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

shader

Shader "MyShader/12-GaussianBlur"
{
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_BlurSize ("Blur Size", Float) = 1.0
	}
	SubShader {
        //CGINCLUDE类似于C++中头文件的功能
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		sampler2D _MainTex;  
        //Unity 提供的MainTexTexelSize计算相邻像素的纹理坐标偏移量
		half4 _MainTex_TexelSize;
		float _BlurSize;
		  
		struct v2f {
			float4 pos : SV_POSITION;
			half2 uv[5]: TEXCOORD0;
		};
		  
		v2f vertBlurVertical(appdata_img v) {
			v2f o;
			o.pos = UnityObjectToClipPos(v.vertex);
			
			half2 uv = v.texcoord;
			// 在本节中我们会利用5x5大小的高斯核对原图像进行高斯模糊
            // 二维高斯核可以拆分成两个大小为5的一维高斯核

            //当前采样
			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 = UnityObjectToClipPos(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};
			//值sum初始化为当前的像素值乘以它的权重值
			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"
}

Bloom效果

Bloom的实现原理非常简单:我们首先根据一个阙值提取出图像中的较亮区域,把它们存储在一张渲染纹理中,再利用高斯模糊对这张渲染纹理进行模糊处理,模拟光线扩散的效果,最后再将其和原图像进行混合,得到最终的效果。

using UnityEngine;
using System.Collections;

//继承基类
public class Bloom : PostEffectsBase {

	public Shader bloomShader;
	private Material bloomMaterial = null;
	public Material material {  
		get {
			//检查材质可用性
			bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, bloomMaterial);
			return bloomMaterial;
		}  
	}

	// 模糊
	[Range(0, 4)]
	public int iterations = 3;
	
	// Blur spread for each iteration - larger value means more blur
	[Range(0.2f, 3.0f)]
	public float blurSpread = 0.6f;

	[Range(1, 8)]
	public int downSample = 2;

	[Range(0.0f, 4.0f)]
	public float luminanceThreshold = 0.6f;

	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			material.SetFloat("_LuminanceThreshold", luminanceThreshold);

			int rtW = src.width/downSample;
			int rtH = src.height/downSample;
			
			RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
			buffer0.filterMode = FilterMode.Bilinear;
			//使用第一个shader提取高光区  存到buffer0
			Graphics.Blit(src, buffer0, material, 0);
			
			for (int i = 0; i < iterations; i++) {
				material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
				
				RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
				
				// 使用垂直方向上pass
				Graphics.Blit(buffer0, buffer1, material, 1);
				
				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
				buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
				
				// 使用水平方向上pass
				Graphics.Blit(buffer0, buffer1, material, 2);

				//释放缓存
				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
			}
			// 将buffer0 传参到	_Bloom
			material.SetTexture ("_Bloom", buffer0);
			// 使用第四个pass进行混合  
			Graphics.Blit (src, dest, material, 3);  
			// 使用后释放缓存
			RenderTexture.ReleaseTemporary(buffer0);
		} else {
			//失败 原样绘制摄像机图像
			Graphics.Blit(src, dest);
		}
	}
}

shader

Shader "MyShader/MotionBlur" {
	Properties {
		_MainTex ("Base (RGB)", 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;
		}
		//用于更新渲染纹理的RGB通道部分
		fixed4 fragRGB (v2f i) : SV_Target {
            //RGB通道版本的Shader对当前图像进行采样,并将其A通道的值设为 BlurAmount
			return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
		}
		//用于更新渲染纹理的A通道部分  
        //之所以要把 A 通道和 RGB 通道分开,是因为在新RGB 时我们需要设置它的A通道来混合图像,但又不希望A通道的值写入染纹理中。
		half4 fragA (v2f i) : SV_Target {
			return tex2D(_MainTex, i.uv);
		}
		
		ENDCG
		
		ZTest Always Cull Off ZWrite Off
		//更新渲染纹理的rbg通道
		Pass {
			Blend SrcAlpha OneMinusSrcAlpha
			ColorMask RGB
			
			CGPROGRAM
			
			#pragma vertex vert  
			#pragma fragment fragRGB  
			
			ENDCG
		}
		//更新渲染纹理的a通道
		Pass {   
			Blend One Zero
			ColorMask A
			   	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment fragA
			  
			ENDCG
		}
	}
 	FallBack Off
}

在这里插入图片描述

运动模糊

运动模糊是真实世界中的摄像机的一种效果。如果在摄像机曝光时,拍摄场景发生了变化,就会产生模糊的画面。运动模糊在我们的日常生活中是非常常见的,只要留心观察,就可以发现无论是体育报道还是各个电影里,都有运动模糊的身影。运动模糊效果可以让物体运动看起来更加真实平滑,但在计算机产生的图像中,由于不存在曝光这一物理现象,渲染出来的图像往往都是清晰的。

运动模糊的实现有多种万法。种实现方法是利用一块累积缓存(accumulation buffer)来混合多张连续的图像。当物体快速移动产生,我们取它们之间的平均值作为最后的运动模糊图像。然而,这种暴力的方法对性能的消耗很大,因为想要获取多张帧图像往往意味着我们需要在同一帧里渲染多次场景。另一种应用广泛的方法是创建和使用速度缓存 (velocitybuffer),这个缓存中存储了各个像素当前的运动速度,然后利用该值来决定模糊的方向和大小。

运动模糊方法一

在本节中,我们将使用类似上述第一种方法的实现来模拟运动模糊的效果。我们不需要在一帧中把场景渲染多次,但需要保存之前的渲染结果,不断把当前的渲染图像叠加到之前的渲染图像中,从而产生一种运动轨迹的视觉效果。这种方法与原始的利用累计缓存的方法相比性能更好但模糊效果可能会略有影响。

在这里插入图片描述

using UnityEngine;
using System.Collections;

public class MotionBlur : PostEffectsBase {

	public Shader motionBlurShader;
	private Material motionBlurMaterial = null;

	public Material material {  
		get {
			//检查并返回材质
			motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, motionBlurMaterial);
			return motionBlurMaterial;
		}  
	}


	[Range(0.0f, 0.9f)] //我们把它的值截取在0.0~0.9范围内 防止代替当前渲染
	public float blurAmount = 0.5f;
	//保存之前图像叠加的结果
	private RenderTexture accumulationTexture;
	//销毁accumulationTexture
	void OnDisable() {
		DestroyImmediate(accumulationTexture); //脚本不运行时销毁accumulationTexture   希望在下一次开始应用运动模糊时重新叠加图像。
	}

	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			// 判断是否满足
			if (accumulationTexture == null || accumulationTexture.width != src.width || accumulationTexture.height != src.height) {
				DestroyImmediate(accumulationTexture);
				//新建一个accumulationTexture
				accumulationTexture = new RenderTexture(src.width, src.height, 0);
				accumulationTexture.hideFlags = HideFlags.HideAndDontSave; // 这意味着这个变量不会显示在 Hierarchy 中,也不会保存到场景中
				//我们使用当前的帧图像初始化accumulationTexture
				Graphics.Blit(src, accumulationTexture); 
			}

			//只是表明我们需要进行一个渲染纹理的操作,来不让Unity误以为是忘记清空了发出警告。
			accumulationTexture.MarkRestoreExpected();

			//传参_BlurAmount
			material.SetFloat("_BlurAmount", 1.0f - blurAmount);
			//每次调用时把当前的屏幕图像src叠加到accumulationTexture中混合
			Graphics.Blit (src, accumulationTexture, material);
			//把结果显示到屏幕上
			Graphics.Blit (accumulationTexture, dest);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

在上面的代码里,我们在该脚本不运行时,即调用OnDisable函数时,立即销毁accumulationTexture。这是因为,我们希望在下一次开始应用运动模糊时重新叠加图像。

在确认材质可用后,我们首先判断用于混合图像的accumulationTexture是否满足条件。我们不仅判断它是否为空,还判断它是否与当前的屏幕分辨率相等,如果不满足,就说明我们需要重新创建一个适合于当前分辨率的accumulationTexture变量。创建完毕后,由于我们会自己控制该变量的销毁,因此可以把它的hideFlags设置为HideFlags.HideAndDontSave,这意味着这个变量不会显示在Hierarchy中,也不会保存到场景中。

然后,我们使用当前的帧图像初始化accumulationTexture (使用Graphics.Blit(src, accumulationTexture)代码)。 当得到了有效的accumulationTexture 变量后,我们调用了accumulationTexture.MarkRestoreExpected函数来表明我们需要进行一个渲染纹理的恢复操作。恢复操作发生在渲染到纹理而该纹理又没有被提前清空或销毁的情况下。在本例中,我们每次调用OnRenderlmage时都需要把当前的帧图像和accumulationTexture 中的图像混合,accumulationTexture纹理不需要提前清空,因为它保存了我们之前的混合结果。然后,我们将参数传递给材质,并调用Graphics.Blit (src, accumulationTexture, materal)把当前的屏幕图像src叠加到accumulationTexture中。最后使用Graphics.Blit (accumulationTexture, dest)把结果显示到屏幕上。
在这里插入图片描述

Shader "MyShader/MotionBlur" {
	Properties {
		_MainTex ("Base (RGB)", 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;
		}
		//用于更新渲染纹理的RGB通道部分
		fixed4 fragRGB (v2f i) : SV_Target {
            //RGB通道版本的Shader对当前图像进行采样,并将其A通道的值设为 BlurAmount
			return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
		}
		//用于更新渲染纹理的A通道部分  
        //之所以要把 A 通道和 RGB 通道分开,是因为在新RGB 时我们需要设置它的A通道来混合图像,但又不希望A通道的值写入染纹理中。
		half4 fragA (v2f i) : SV_Target {
			return tex2D(_MainTex, i.uv);
		}
		
		ENDCG
		
		ZTest Always Cull Off ZWrite Off
		//更新渲染纹理的rbg通道
		Pass {
			Blend SrcAlpha OneMinusSrcAlpha
			ColorMask RGB
			
			CGPROGRAM
			
			#pragma vertex vert  
			#pragma fragment fragRGB  
			
			ENDCG
		}
		//更新渲染纹理的a通道
		Pass {   
			Blend One Zero
			ColorMask A
			   	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment fragA
			  
			ENDCG
		}
	}
 	FallBack Off
}

RGB通道版本的Shader对当前图像进行采样,并将其A通道的值设为_BlurAmount, 以便在后面混合时可以使用它的透明通道进行混合。A通道版本的代码就更简单了,直接返回采样结果。实际上,这个版本只是为了维护渲染纹理的透明通道值,不让其受到混合时使用的透明度值的影响。 之所以要把A通道和RGB通道分开,是因为在更新RGB时我们需要设置它的A通道来混合图像,但又不希望A通道的值写入渲染纹理中。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值