Unity Shader入门精要 第十二章——屏幕后处理效果

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

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

        首先我们要得到渲染后的屏幕图像,Unity提供了这个接口——OnRenderImage函数。函数声明如下:

        MonoBehaviour.OnRenderImage(RenderTexture src, RenderTexture dest)

        我们在脚本声明此函数后,Unity会把当前渲染得到的图像存储在第一个参数对应的渲染纹理中,通过函数中的一系列操作后,再把目标纹理也就是第二个参数对应的渲染纹理显示到屏幕上。OnRenderImage函数通常是利用Graphics.Blit函数来完成对渲染纹理的处理。对应的有三种函数声明:

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);

        src对应源纹理,通常就是当前屏幕的渲染纹理或是上一步处理后得到的渲染纹理。dest是目标渲染纹理,如果dest的值为null就会直接将结果显示在屏幕上。mat是我们使用的材质,pass的值默认为-1,表示将会依次调用mat的Shader内的所有pass。否则只会调用给定索引的pass。

        默认情况下,OnRenderImage函数·会在所有的不透明和透明的Pass执行完毕后被调用,就可以对场景中所有的游戏对象都产生影响。如果我们希望不会对透明物体产生影响,我们可以在OnRenderImage函数前面添加ImageEffectQpaque属性来实现这样的效果。

        实现屏幕后处理效果的过程如下:

  1. 检查一系列条件是否满足,如:当前平台是否支持渲染纹理和屏幕特效,是否支持Unity Shader等。为此我们可以创建了一个用于屏幕后处理效果的基类,我们在实现各种屏幕效果时只需要继承这个积累,再实现派生类中不同的操作即可。
  2. 给摄像机添加一个用于屏幕后处理的脚本。在这个脚本会实现OnRenderImage函数来获取当前屏幕的渲染纹理。
  3. 在OnRenderImage函数中调用Graphics.Blit函数使用特定的Unity Shader对当前图像进行处理,我们也可以调用多次Graphics.Blit函数,来对上一步的输出结果进行下一步处理。最后把渲染纹理显示到屏幕上。

        为此我们创建了一个用于屏幕后处理效果的基类,在实现各种屏幕特效时,我们只需要继承该积累,再实现派生类中不同的操作即可。代码如下:

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]//可以在编辑器状态下也可以执行该脚本
[RequireComponent (typeof(Camera))]//确保有Camera组件
public class PostEffectsBase : MonoBehaviour {

	// 在start中调用该函数,用来提前检查各种资源和条件是否满足
	protected void CheckResources() {
		bool isSupported = CheckSupport();
		
		if (isSupported == false) {
			NotSupported();
		}
	}

	// Called in CheckResources to check support on this platform
	// 检查平台是否支持
	protected bool CheckSupport() {
		if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false) {
			Debug.LogWarning("This platform does not support image effects or render textures.");
			return false;
		}
		
		return true;
	}


	//平台不支持的时候使用该函数
	protected void NotSupported() {
		enabled = false;
	}
	
	protected void Start() {
		CheckResources();
	}
	//指定一个Shader来创建一个用于处理渲染纹理的材质
	protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
		//如果shader为null,返回null
		if (shader == null) {
			return null;
		}
		//判断是否支持这个shader的运行 、material不为null、material的shader就是传入的shader
		if (shader.isSupported && material && material.shader == shader)
			return material;//直接返回材质
		
		if (!shader.isSupported) {//不支持返回null
			return null;
		}
		else {//支持,但是material为null,或者material的shader不是我们要的shader
			material = new Material(shader);//重新给shader赋值
			material.hideFlags = HideFlags.DontSave;//这个,枚举类型表示保留对象到新场景
			if (material)
				return material;
			else 
				return null;
		}
	}
}

        下面我们来使用这个基类来做一个简单的特效脚本。

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

        创建一个C#脚本,继承上面的基类,然后搭载在摄像机上。

using UnityEngine;
using System.Collections;

public class BrightnessSaturationAndContrast : PostEffectsBase {
	//声明该效果需要的Shader,并据此创建相应的材质
	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;

	//定义OnRenderImage函数来进行真正的特效处理
    //Unity会把当前渲染得到的图像存储在第一个参数对应的渲染纹理中
	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);//会把第一个参数传给_MainTex
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

        创建一个Unity Shader文件拖拽给上面的脚本

Shader "MyShader/Chapter 12/Brightness Saturation And Contrast" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}//Graphics.Blit会把第一个参数传给_MainTex
		_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;
			};
			  
			v2f vert(appdata_img v) {//这里使用了Unity内置的appdata_img结构体作为顶点着色器的输入。
				v2f o;
				
				o.pos = UnityObjectToClipPos(v.vertex);
				
				o.uv = v.texcoord;
						 
				return o;
			}
		
			fixed4 frag(v2f i) : SV_Target {//SV_Target:输出值直接用于渲染了  
				fixed4 renderTex = tex2D(_MainTex, i.uv);  //得到原屏幕图像的采样结果
				  
				//调整亮度:原颜色*亮度系数_Brightness
				fixed3 finalColor = renderTex.rgb * _Brightness;
				
				//计算该像素对应的亮度值(luminance)
				fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
				//使用该亮度值创建了一个饱和度为0的颜色
				fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
				//根据饱和度系数,在luminanceColor和finalColor之间插值
				finalColor = lerp(luminanceColor, finalColor, _Saturation);
				
				//创建对比度为0的颜色(各分量都为0.5)
				fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
				//再使用_Contrast属性,在avgColor和finalColor之间插值
				finalColor = lerp(avgColor, finalColor, _Contrast);
				
				return fixed4(finalColor, renderTex.a);  
			}  
			  
			ENDCG
		}  
	}
	
	Fallback Off//关闭Unity Shader的Fallback
}

        然后我们就可以在摄像机的脚本下面调整屏幕的亮度、饱和度、对比度

12.3 边缘检测

12.3.1 什么是卷积

        卷积操作指定是使用一个卷积核(kernel)对一张图像中的每个像素进行一系列的操作。卷积核通常是一个四方形网格结构,该方格每个区域都有一个权重值。对图像中的某个像素进行卷积时,我们会把卷积核的中心位置放置于该像素上(如下图),翻转核之后依次计算核中每个元素和其覆盖的图像像素值的乘积并求和,得到的就是该位置的新像素值。

         不同的卷积和就可以得到不同的效果

计算核中每个元素和其覆盖的图像像素值的乘积并求和

12.3.2 常见的边缘检测算子(用于边缘检测的卷积核)

         如果相邻像素之间存在差别明显的颜色、亮度、纹理等属性,我们就会认为他们之间一个有一条边界。这种相邻像素之间的插值可以用梯度(gradient)来表示,所以边缘处的梯度值的绝对值会比较大。

        上面是三种常见的边缘检测算子,都包含了两个方向的卷积核,分别检测水平方向核竖直方向商店边缘信息。在进行边缘检测的时候我们需要对每个像素分别进行依次卷积计算,得到Gx核Gy,整体的梯度如下计算:

G = \sqrt{G_{x}^{2}+G_{y}^{2}}

        上面的公式有开根号,考虑到性能,有时候会使用绝对值操作来代替开根号:

G = |G_{x}| +|G_{y}|

        得到了梯度G之后我们就可以据此来判断哪些像素对应了边缘(梯度越大,越有可能是边缘点)。

12.3.3 实现

        下面我们使用Soble算子来进行边缘检测。

        首先创建一个C#脚本搭载在Camera上

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;//边缘线强度,为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);
		}
	}
}

        然后创建一个Shader拖拽给上面这个脚本

Shader "MyShader/Chapter 12/Edge Detection" {
	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;  
			//_TexelSize是 unity为我们提供的访问纹理对应的每个纹素的大小
			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;
				//uv为一个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; 
			}
			
			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};		
				
				half texColor;
				half edgeX = 0;
				half edgeY = 0;
				//依次对9个像素进行采样,计算亮度值,再与卷积核Gx和Gy中对应的权重相乘后,叠加到格子的梯度值上
				for (int it = 0; it < 9; it++) {
					texColor = luminance(tex2D(_MainTex, i.uv[it]));
					edgeX += texColor * Gx[it];
					edgeY += texColor * Gy[it];
				}
				//最后我们从1减去edgeX和edgeY的绝对值,得到edge,edge越小,表面该位置越可能是一个边缘点
				half edge = 1 - abs(edgeX) - abs(edgeY);
				
				return edge;
			}
			
			fixed4 fragSobel(v2f i) : SV_Target {
				half edge = Sobel(i);//调用Sobel函数计算梯度值edge
				//利用edge计算了背景为原图和纯色下的颜色值
				fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
				fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
				//最后在两者之间进行插值得到最终像素值
				return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
 			}
			
			ENDCG
		} 
	}
	FallBack Off
}

        最后我们就可以得到下面的效果啦,下面分别是edges only值为0,和值为1的情况

12.4 高斯模糊

        很多模糊的方法都采样了卷积操作,如均值模糊,中值模糊,高斯模糊。均值模糊使用的卷积核各个元素值都相等,且相加等于1,也就是说卷积得到的图像值是其邻域内各个像素值的平均值;中值模糊是选择邻域内对所有像素排序后的中值替换掉原颜色;高斯模糊更为高级,下面来讲解。

12.4.1 高斯滤波

        高斯模糊使用的卷积核名为高斯核,其中每个元素的计算都是基于高斯方程:

          \sigma是标准方差(一般取值为1),x和y对应了当前位置到卷积核中心的整数距离。要构建一个高斯核,我们只需要计算高斯核中各个位置对应的高斯值。为了保证滤波后的图像不会变暗,我们需要对高斯核中的权重进行归一化,即让每个权重除以所有权重的和,这样可以保证所有权重的和为1。因此,高斯函数中e前面的系数实际不会对结构有任何影响。

        高斯方程很好的模拟了邻域每个像素对当前像素的影响程度——距离越近,影响越大。高斯核的维数越高,模糊程度越大。使用一个N*N的高斯核进行卷积滤波,就需要N*N*W*H次纹理采样(W、H是宽高)。但我们可以把这个二维函数拆分为两个一维函数,这样只需要2*N*W*H次采样。

        下面我们将用这个5*5的高斯核进行高斯模糊。会使用到两个pass,第一个pass使用竖直方向的一维高斯核对图像进行滤波,第二个pass使用水平方向的一维高斯核对图像进行滤波。我们还会利用图像缩放进一步提高性能,并通过调整滤波的应用次数来控制模糊程度(次数越多,图像越模糊)。

12.4.1 实现

        首先创建一个搭载在Camera上的C#脚本。

using UnityEngine;
using System.Collections;

public class GaussianBlur : PostEffectsBase {

	public Shader gaussianBlurShader;
	private Material gaussianBlurMaterial = null;

	public Material material {  
		get {
			gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, gaussianBlurMaterial);
			return gaussianBlurMaterial;
		}  
	}

	// Blur iterations - larger number means more blur.
	//高斯模糊迭代系数
	[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;

	void OnRenderImage (RenderTexture src, RenderTexture dest) {
		if (material != null) {
			int rtW = src.width/downSample;
			int rtH = src.height/downSample;

			//利用RenderTexture.GetTemporary函数分配了一块与屏幕图像大小相同的缓冲区,用来储存过程中的结果
			RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
			buffer0.filterMode = FilterMode.Bilinear;
			//把src的图像缩放和存储到buffer0中
			Graphics.Blit(src, buffer0);

			//iterations迭代次数
			for (int i = 0; i < iterations; i++) {
				material.SetFloat("_BlurSize", 1.0f + i * blurSpread);

				RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

				// Render the vertical pass
				// 迭代竖直方向的pass
				Graphics.Blit(buffer0, buffer1, material, 0);
				// 调用 RenderTexture ReleaseTemporary 来释放之前分配的缓存。
				// 释放buffer0,然后把结果值存储到buffer1中
				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
				buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);

				// Render the horizontal pass
				// 迭代水平方向的pass
				Graphics.Blit(buffer0, buffer1, material, 1);
				// 释放buffer0,然后把结果值存储到buffer1中
				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
			}
			// 循环迭代次数后,最后显示到屏幕上,并释放缓存
			Graphics.Blit(buffer0, dest);
			RenderTexture.ReleaseTemporary(buffer0);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

        然后创建一个Shader拖拽给上面这个脚本

Shader "MyShader/Chapter 12/Gaussian Blur" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}//输入的渲染纹理
		_BlurSize ("Blur Size", Float) = 1.0
	}
	SubShader {
		CGINCLUDE//使用 CGINCLUDE 来组织代码
		
		//在CGINCLUDE和下面的ENDCG之间的代码不需要包括在任何Pass语句块中,
		//在使用时,我们只需要在Pass直接指定需要使用的顶点着色器和片元着色器函数名即可
		//CGINCLUDE 类似于 ++中头文件的功能。由于高斯模糊需要定义两 Pass 但它们使用的片元着色器代码是完全相同的 
		//使用 CGINCLUDE 可以避免我们编写两个完全一样的 frag 函数。
		#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 = UnityObjectToClipPos(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 = 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};
			
			//对本身像素进行采样
			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//对应上面的CGINCLUDE
		
		ZTest Always Cull Off ZWrite Off
		
		Pass {
			//为 Pass 定义名字,可以在其他Shader 中直接通过它们的名字来使用该 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"
}

12.5 Bloom 效果

        Bloom特性是模拟真实摄像机的一种图像效果,可以让画面较亮的区域“扩散”到周围区域中,造成一种朦胧的效果。

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

        C#(搭载在相机上):

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;
		}  
	}

	// Blur iterations - larger number means more blur.
	[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;
			
			//使用第一个Pass提取图像中较亮区域。
			Graphics.Blit(src, buffer0, material, 0);
			
			//下面for循环对较亮区域进行高斯模糊
			for (int i = 0; i < iterations; i++) {
				material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
				
				RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
				
				// Render the vertical pass
				Graphics.Blit(buffer0, buffer1, material, 1);
				
				RenderTexture.ReleaseTemporary(buffer0);
				buffer0 = buffer1;
				buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
				
				// Render the horizontal 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/Chapter 12/Bloom" {
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		_Bloom ("Bloom (RGB)", 2D) = "black" {}//高斯模糊后的较亮区域
		_LuminanceThreshold ("Luminance Threshold", Float) = 0.5//提取较亮区域的阈值
		_BlurSize ("Blur Size", Float) = 1.0
	}
	SubShader {
		CGINCLUDE
		
		#include "UnityCG.cginc"
		
		sampler2D _MainTex;
		half4 _MainTex_TexelSize;
		sampler2D _Bloom;
		float _LuminanceThreshold;
		float _BlurSize;
		
		struct v2f {
			float4 pos : SV_POSITION; 
			half2 uv : TEXCOORD0;
		};	
		
		v2f vertExtractBright(appdata_img v) {
			v2f o;
			
			o.pos = UnityObjectToClipPos(v.vertex);
			
			o.uv = v.texcoord;
					 
			return o;
		}
		//计算亮度值
		fixed luminance(fixed4 color) {
			return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b; 
		}
		
		fixed4 fragExtractBright(v2f i) : SV_Target {
			fixed4 c = tex2D(_MainTex, i.uv);
			//将采样得到的亮度值减去阈值,并把结果截取到0~1范围内
			fixed val = clamp(luminance(c) - _LuminanceThreshold, 0.0, 1.0);
			//于原像素值相乘得到提取后的亮部区域
			return c * val;
		}
		
		struct v2fBloom {
			float4 pos : SV_POSITION; 
			half4 uv : TEXCOORD0;//定义了两个纹理坐标,存储在uv变量中,xy对于_MainTex,zw对于_Bloom
		};
		
		v2fBloom vertBloom(appdata_img v) {
			v2fBloom o;
			
			o.pos = UnityObjectToClipPos (v.vertex);
			o.uv.xy = v.texcoord;		
			o.uv.zw = v.texcoord;
			//平台差异化处理
			#if UNITY_UV_STARTS_AT_TOP			
			if (_MainTex_TexelSize.y < 0.0)
				o.uv.w = 1.0 - o.uv.w;
			#endif
				        	
			return o; 
		}
		
		fixed4 fragBloom(v2fBloom i) : SV_Target {
			return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);
		} 
		
		ENDCG
		
		ZTest Always Cull Off ZWrite Off
		
		Pass {  
			CGPROGRAM  
			#pragma vertex vertExtractBright  
			#pragma fragment fragExtractBright  
			
			ENDCG  
		}
		
		UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_VERTICAL"
		
		UsePass "Unity Shaders Book/Chapter 12/Gaussian Blur/GAUSSIAN_BLUR_HORIZONTAL"
		
		Pass {  
			CGPROGRAM  
			#pragma vertex vertBloom  
			#pragma fragment fragBloom  
			
			ENDCG  
		}
	}
	FallBack Off
}
使用前
使用后

12.6 运动模糊

        实现多态模糊的两种方法:

  • 累计缓存(accumulation buffer);来混合多张连续的图像。当物体快速移动产生多张图像后,我们取他们之间的平均值作为最后的运动模糊图像。需要一帧中渲染多次场景,对性能消耗很大。
  • 速度缓存(velocity buffer);这个缓存存储了各个像素当前的运动速度,任何利用该值决定模糊的方向和大小。

        下面我们使用类似于第一种方法来实现多态模糊,但不需要一帧渲染多次场景,但需要保存之前渲染的结果,不断把当前的渲染图像叠加到之前的渲染图像中。这个方法比累计缓存效果更好,但是模糊效果可能略有影响。

        C#(搭载在相机上):

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;
		}  
	}
	//这个数值越大,运动拖尾的效果就越明显。为了防止拖尾效果完全代替当前帧的渲染结果,限制在0~0.9
	[Range(0.0f, 0.9f)]
	public float blurAmount = 0.5f;
	//保存之前图像叠加的结果。
	private RenderTexture accumulationTexture;

	void OnDisable() {
		//脚本不运行时,销毁accumulationTexture,因为我们希望下一次运行时重新叠加图像
		DestroyImmediate(accumulationTexture);
	}

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

			//MarkRestoreExpected()函数表明我们需要进行一个渲染纹理的恢复操作。
			//恢复操作发生在渲染到纹理而该纹理又没有被提前清空或销毁的情况下。
			//accumulationTexture纹理不需要提前清空,因为它保存了之前的混合结果
			accumulationTexture.MarkRestoreExpected();

			material.SetFloat("_BlurAmount", 1.0f - blurAmount);

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

                Shader:

Shader "MyShader/Chapter 12/Motion Blur" {
	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通道部分,另一个用于更新渲染纹理的A通道部分
		fixed4 fragRGB (v2f i) : SV_Target {
			return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
		}
		//把两个分开是因为在更新RGB时我们需要设置它的A通道来混合图像,但又不希望A通道的值写入渲染纹理中
		half4 fragA (v2f i) : SV_Target {
			return tex2D(_MainTex, i.uv);
		}
		
		ENDCG
		
		ZTest Always Cull Off ZWrite Off
		
		Pass {
			Blend SrcAlpha OneMinusSrcAlpha
			ColorMask RGB
			
			CGPROGRAM
			
			#pragma vertex vert  
			#pragma fragment fragRGB  
			
			ENDCG
		}
		
		Pass {   
			Blend One Zero
			ColorMask A
			   	
			CGPROGRAM  
			
			#pragma vertex vert  
			#pragma fragment fragA
			  
			ENDCG
		}
	}
 	FallBack Off
}

         下面是动态模糊前和动态模糊后的对比。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Unity Shader是一种用于渲染图形的程序,它可以控制对象的表面颜色、纹理、透明度、反射等属性,从而实现特殊的视觉效果。对于游戏开发者来说,掌握Shader编写技巧是非常重要的。 以下是关于Unity Shader入门精要: 1. ShaderLab语言 ShaderLab是Unity中用于编写Shader的语言,它是一种基于标记的语言,类似于HTML。ShaderLab可以用于定义Shader的属性、子着色器、渲染状态等信息。 2. CG语言 CG语言是Unity中用于编写Shader的主要语言,它是一种类似于C语言的语言,可以进行数学运算、向量计算、流程控制等操作。CG语言可以在ShaderLab中嵌入,用于实现Shader的具体逻辑。 3. Unity的渲染管线 Unity的渲染管线包括顶点着色器、片元着色器、几何着色器等组件,每个组件都有不同的作用。顶点着色器用于对对象的顶点进行变换,片元着色器用于计算每个像素的颜色,几何着色器用于处理几何图形的变形和细节等。 4. 模板和纹理 在Shader中,我们可以使用纹理来给对象添加图案或者贴图,也可以使用模板来控制对象的透明度、反射等属性。纹理可以通过内置函数tex2D()来获取,模板可以通过内置函数clip()来实现裁剪。 5. Shader的实现 Shader的实现需要注意以下几点: - 在ShaderLab中定义Shader的属性、子着色器、渲染状态等信息。 - 在CG语言中实现Shader的具体逻辑,包括顶点着色器、片元着色器等内容。 - 使用纹理和模板来实现特定的视觉效果。 - 在对象上应用Shader,通过调整Shader的属性来达到不同的效果。 以上是关于Unity Shader入门精要,希望对你有所帮助。如果你想更深入地了解Shader的编写技巧,可以参考官方文档或者相关教程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

buzhengli

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值