1. 程式人生 > 其它 >unity Shader 後處理實現邊緣檢測

unity Shader 後處理實現邊緣檢測

本文記錄用sobel運算元進行邊緣檢測,實現unity描邊屏幕後處理效果的過程(Learn by 《unity shader 入門精要》)

unity實現屏幕後處理效果過程如下:

1、首先在攝像機中新增一個用於屏幕後處理的指令碼,該指令碼需要先檢測一系列條件是否滿足 如當前平臺是否支援渲染紋理和螢幕特效,是否支援當前的unity shader。為了提高程式碼複用性,我們還建立了一個基類用於檢測條件,將具體實現效果的指令碼繼承自該基類。

基類PostEffectBase.cs

using UnityEngine;
using System.Collections;

[ExecuteInEditMode]//支援指令碼在編輯模式下執行
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {
    //protected void Start()
    //{
    //	CheckResources();
    //}

    // Called when start
    //protected void CheckResources()
    //{
    //    bool isSupported = CheckSupport();

    //    if (isSupported == false)
    //    {
    //        NotSupported();
    //    }
    //}

    // 檢查平臺支援 現已無需檢測,始終返回true
    //protected bool CheckSupport() {
    //	if (SystemInfo.supportsImageEffects == false) {
    //		Debug.LogWarning("This platform does not support image effects.");
    //		return false;
    //	}

    //	return true;
    //}

    // Called when the platform doesn't support this effect
    //protected void NotSupported() {
    //	enabled = false;
    //}


    // Called when need to create the material used by this effect
    protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
		if (shader == null) {
			return null;
		}
		
		//if (shader.isSupported && material && material.shader == shader)
		//	return material;

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

邊緣檢測指令碼 EdgeDetection.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; //0——1  原影象——邊緣

	public Color edgeColor = Color.black; //邊緣顏色
	
	public Color backgroundColor = Color.white; //背景顏色

	//src——源紋理  dest——目標紋理
	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);
		}
	}
}

edgesOnly:用於調整邊緣與源影象的混合權重 當edgesOnly為1時,則只會顯示邊緣,為0時則會疊加在源渲染影象上

 

OnRenderImage:是unity提供的介面,方便我們直接抓取渲染後的螢幕影象。在該函式中,我們通常利用Graphics.Blit函式來完成對渲染紋理的處理

 

Blit:其宣告如下 public static void Blit(Texture src,RenderTexture dest,Material mat,int pass=-1)

src對應源紋理,

dest是目標紋理,

mat是我們使用的材質,他會將src紋理傳遞給Shader中名為_MainTex的紋理屬性

pass 預設-1,表示他會依次呼叫Shader內所有Pass

 

 2、建立一個Shader用於處理渲染紋理,基類提供的CheckShaderAndCreateMaterial方法會自動返回一個使用該shader的材質

 Edge Detection.shader

Shader "MyShader/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;  
			uniform half4 _MainTex_TexelSize;//訪問某紋理對應的每個紋素大小。通過其計算各個相鄰區域的紋理座標
			fixed _EdgeOnly;
			fixed4 _EdgeColor;
			fixed4 _BackgroundColor;
			
			struct v2f {
				float4 pos : SV_POSITION;
				half2 uv[9] : TEXCOORD0;
			};
			//appdata_img為unity內建結構體 包含一個頂點和一個紋理資訊
			v2f vert(appdata_img v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				half2 uv = v.texcoord;
				
				//定義維數為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) {
				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減去水平方向和豎直方向的梯度值的絕對值,得到edge edge越小越可能是邊緣
				half edge = 1 - abs(edgeX) - abs(edgeY);
				
				return edge;
			}
			
			fixed4 fragSobel(v2f i) : SV_Target {
				half edge = Sobel(i);
				
				//lerp(from,to,t) = from + (to - from) *t
				fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
				fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
				return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
 			}
			
			ENDCG
		} 
	}
	FallBack Off
}

最後片元著色fragSobel的3個Lerp操作有點難理解

首先搞清楚lerp的數學意義

lerp(from,to,t)=from + (to - from)*t 也就是隨t 從0到1 輸出結果從 from 到 to

從前面卷積得到的邊緣來看,邊緣edge越小越有可能是邊緣

第一個withEdgeColor的函式lerp的引數是_EdgeColor和螢幕源影象的取樣,也就是lerp從0—1的變化時從帶邊緣的圖到不帶邊緣的圖

第二個onlyEdgeColor的函式lerp的第二個引數變換為了背景色(預設為白色),也就是lerp從0——1的變化時從只有邊緣的圖到僅有背景色的圖

最後return的lerp引數是前兩個lerp的返回值,lerp從0——1變化時從帶邊緣的圖到只有邊緣的圖

 

3、編寫完程式碼返回編輯器後,在EdgeDetection.cs指令碼面板將edgeDetectShader拖拽到公開變數中

展示如下: