UnityShader学习——屏幕后处理效果(亮度等、边缘检测)

什么是屏幕后处理

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

想要实现屏幕后处理的基础在于得到渲染后的屏幕图像,即抓取屏幕,而Unity为我们提供了这样一个方便的接口——OnRenderImage函数。它的函数声明如下:

MonoBehavior.OnRenderImage(RenderTexture src,RenderTexture dest)
  • src:当前渲染得到的图像对应的源渲染纹理。
  • dest:通过函数中的一系列操作后得到的目标渲染纹理。Unity会把此纹理显示到屏幕上。

在OnRenderImage函数中,我们通常是利用Graphics.Blit函数来完成对渲染纹理的处理。它有3种函数声明:

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:源纹理。在屏幕后处理技术中,这个参数通常就是当前屏幕的渲染纹理或是上一步处理后得到的渲染纹理。
  • dest:目标渲染纹理,如果它的值为null就会直接将结果显示在屏幕上。
  • mat:我们使用的材质,这个材质使用的Unity Shader将会进行各种屏幕后处理操作,而src纹理将会被传递给Shader中名为_MainTex的纹理属性。
  • pass:默认值为-1,表示将会依次调用Shader内的所有Pass。否则,只会调用给定索引的Pass。

渲染顺序

在默认情况下,OnRenderImage函数会在所有的不透明和透明的Pass执行完毕后被调用,以便对场景中所有游戏对象都产生影响。

但有时,我们希望在不透明的Pass(即渲染队列小于等于2500的Pass,内置的Background、Geometry和AlphaTest渲染队列均在此范围内)执行完毕后立即调用OnRenderImage函数,从而不对透明物体产生任何影响。

此时,我们可以在OnRenderImage函数前添加ImageEffectOpaque属性来实现这样的目的。

Unity实现屏幕后处理的一般流程

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

屏幕后处理基类(检查可用性)

代码如下:

using UnityEngine;
using System.Collections;

//希望在编辑器状态下也可以执行该脚本来查看效果
[ExecuteInEditMode]
//需要绑定在某个摄像机上
[RequireComponent (typeof(Camera))] 
public class PostEffectsBase : MonoBehaviour {
	
	protected void Start() {
		CheckResources();//在Start函数中调用CheckResources函数
	}
	
	protected void CheckResources() {
		bool isSupported = CheckSupport();		
		if (isSupported == false) {
			NotSupported();
		}
	}

	//平台是否支持屏幕特效
	protected bool CheckSupport() {
		if (SystemInfo.supportsImageEffects == false) {
			Debug.LogWarning("This platform does not support image effects.");
			return false;
		}		
		return true;
	}

	//不支持屏幕特效时
	protected void NotSupported() {
		enabled = false;
	}
	//每个屏幕后处理效果通常都需要指定一个Shader来创建一个用于处理渲染纹理的材质
	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;
		}
	}
}

常用屏幕效果

1.亮度、饱和度、对比度

摄像机脚本:每当OnRenderImage函数被调用时,它会检查材质是否可用。如果可用,就把参数传递给材质,再调用Graphics.Blit进行处理;否则,直接把原图像显示到屏幕上,不做任何处理。

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;
		}  
	}
	//调整亮度、饱和度和对比度的参数
	//利用 Unity提供的 Range属性为每个参数提供合适的变化区间
	[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函数来进行真正的特效处理
	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关键代码:

	Properties {
		//【1】Graphics.Blit(src, dest, material)将把第一个参数传递给Shader中名为_MainTex的属性
		//必须声明一个名为_MainTex的纹理属性
		_MainTex ("Base (RGB)", 2D) = "white" {}
		//用于调整亮度、饱和度和对比度的属性。这些值将会由脚本传递而得
		//可以省略Properties中的属性声明,这些材质都是临时创建的
		_Brightness ("Brightness", Float) = 1
		_Saturation("Saturation", Float) = 1
		_Contrast("Contrast", Float) = 1
	}
	SubShader {
		Pass { 			
			//【2】屏幕后处理的Shader的“标配”
			//屏幕后处理实际上是在场景中绘制了一个与屏幕同宽同高的四边形面片
			ZTest Always 
			Cull Off 
			//关闭了深度写入,是为了防止它“挡住”在其后面被渲染的物体
			ZWrite Off
			
			CGPROGRAM  
			......		
			fixed4 frag(v2f i) : SV_Target {
				//【3】得到对原屏幕图像(存储在_MainTex中)的采样结果renderTex
				fixed4 renderTex = tex2D(_MainTex, i.uv);  
				  
				//【4】亮度的调整非常简单,我们只需要把原颜色乘以亮度系数_Brightness即可
				fixed3 finalColor = renderTex.rgb * _Brightness;
				
				//【5】饱和度调整:
				//对每个颜色分量乘以一个特定的系数再相加得到该像素对应的亮度值(luminance)			
				fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;	
				//使用该亮度值创建了一个饱和度为0的颜色值
				fixed3 luminanceColor = fixed3(luminance, luminance, luminance);
				//使用_Saturation属性在其和上一步得到的颜色之间进行插值,从而得到希望的饱和度颜色
				finalColor = lerp(luminanceColor, finalColor, _Saturation);
				
				//【6】对比度调整:
				//创建一个对比度为0的颜色值(各分量均为0.5)
				fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
				//使用_Contrast属性在其和上一步得到的颜色之间进行插值
				finalColor = lerp(avgColor, finalColor, _Contrast);
				
				return fixed4(finalColor, renderTex.a);  
			}  
			  
			ENDCG
		}  
	}	
	Fallback Off
在这里插入图片描述 在这里插入图片描述

2.边缘检测

边缘检测的原理是利用一些边缘检测算子对图像进行卷积(convolution)操作。需要注意的是,本节实现的边缘检测仅仅利用了屏幕颜色信息,而在实际应用中,物体的纹理、阴影等信息均会影响边缘检测的结果,使得结果包含许多非预期的描边。为了得到更加准确的边缘信息,我们往往会在屏幕的深度纹理和法线纹理上进行边缘检测。

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

边缘处的梯度绝对值会比较大。基于这样的理解,有几种不同的边缘检测算子被先后提出来。
在这里插入图片描述
它们都包含了两个方向的卷积核,分别用于检测水平方向和竖直方向上的边缘信息。在进行边缘检测时,我们需要对每个像素分别进行一次卷积计算,得到两个方向上的梯度值 G x G_x G y G_y ,而整体的梯度可按下面的公式计算而得:
在这里插入图片描述出于性能的考虑,我们有时会使用绝对值操作来代替开根号操作。当得到梯度G后,我们就可以据此来判断哪些像素对应了边缘(梯度值越大,越有可能是边缘点)。

摄像机脚本关键代码(其他代码和上一个代码类似):

......
public class EdgeDetection : PostEffectsBase {
	......
	[Range(0.0f, 1.0f)]
	public float edgesOnly = 0.0f;
	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关键代码:

	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	SubShader {
		Pass {  
			......			
			CGPROGRAM			
			......
			#pragma fragment fragSobel
			
			sampler2D _MainTex;  
			//xxx_TexelSize是Unity为我们提供的访问xxx纹理对应的每个纹素的大小
			//卷积需要对相邻区域内的纹理进行采样
			//所以需要利用_MainTex_TexelSize来计算各个相邻区域的纹理坐标
			uniform half4 _MainTex_TexelSize;
			//当edgesOnly值为0时,边缘将会叠加在原渲染图像上;
			//当edgesOnly值为1时,则会只显示边缘,不显示原渲染图像
			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算子对原图进行边缘检测
			half Sobel(v2f i) {
				//【1】定义水平方向和竖直方向使用的卷积核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;						
				for (int it = 0; it < 9; it++) {
					//【2】依次对9个像素进行采样,计算它们的亮度值
					texColor = luminance(tex2D(_MainTex, i.uv[it]));
					//【3】与卷积核Gx和Gy中对应的权重相乘后,叠加到各自的梯度值上
					edgeX += texColor * Gx[it];
					edgeY += texColor * Gy[it];
				}				
				//【4】从1中减去水平方向和竖直方向的梯度值的绝对值,得到edge
				half edge = 1 - abs(edgeX) - abs(edgeY);				
				return edge;
			}
			
			fixed4 fragSobel(v2f i) : SV_Target {
				//调用Sobel函数计算当前像素的梯度值edge
				half edge = Sobel(i);
				//利用_EdgeOnly在两者之间插值得到最终的像素值
				fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
				fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
				return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
 			}			
			ENDCG
		} 
	}
	FallBack Off

在这里插入图片描述

发布了195 篇原创文章 · 获赞 59 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_36622009/article/details/105602156