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拖拽到公開變數中
展示如下: