本文记录用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拖拽到公开变量中

展示如下:

unity 测试网络延时_卷积