Unity Shader - 边缘检测

边缘检测(英语:Edge detection)是图像处理和计算机视觉中的基本问题,边缘检测的目的是标识数字图像中亮度变化明显的点。图像属性中的显著变化通常反映了属性的重要事件和变化。这些包括(i)深度上的不连续、(ii)表面方向不连续、(iii)物质属性变化和(iv)场景照明变化。 边缘检测是图像处理和计算机视觉中,尤其是特征检测中的一个研究领域。

原理

边缘检测实质上就是通过对图像的卷积的结果,卷积是数学分析中一种重要的运算,主要区别在于卷积核的选择,通过卷积操作我们不仅可以做到边缘检测,还可以做到模糊图像锐化图像等功能。

“模糊图像” 可以参考我的这篇文章

另外的描边 Shader可以参考这篇

边缘检测的卷积核 也叫做 边缘检测算子
如:Roberts Cross算子, Prewitt算子, Sobel算子, Canny算子,罗盘算子

这里以Sobel算子为例子:

G x = [ + 1 0 − 1 + 2 0 − 2 + 1 0 − 1 ] ∗ A G y = [ + 1 2 + 1 0 0 0 − 1 − 2 − 1 ] ∗ A G_x = \begin{bmatrix}+1&0& -1\\\\+2&0& -2\\\\+1&0& -1\end{bmatrix} *A \qquad G_y = \begin{bmatrix}+1&2&+1\\\\0&0& 0\\\\-1&-2& -1\end{bmatrix}*A Gx=+1+2+1000121AGy=+101202+101A

A {A} A代表原始图像, G x 和 G y G_x和 G_y GxGy分别代表经横向及纵向边缘检测的图像,

通过以上公式就可以分别计算出横向纵向梯度值,即 G x 和 G y G_x和 G_y GxGy
梯度值越大,边缘就越明显。

注意:

  • 计算梯度值前需要先把图像置灰,方便我们计算梯度值
  • 横向的梯度值检测出来的是纵向的边缘线,纵向的梯度值检测出来的是横向的边缘线。

以上是理论,可能有些小伙伴看得很懵逼,下面我们举一个例子来说明一下:
在这里插入图片描述   在这里插入图片描述   在这里插入图片描述
假如上图是我们在图像上随机选取的两块3*3的像素块,以及横向的Sobel算子

像素块一的梯度值: G x 1 = 186 ∗ 1 + 186 ∗ 2 + 186 ∗ 1 − 233 ∗ 1 − 233 ∗ 2 − 232 ∗ 1 = − 187 G_{x1}=186*1 + 186*2 + 186*1 - 233*1- 233*2- 232*1=-187 Gx1=1861+1862+1861233123322321=187
像素块二的梯度值: G x 2 = 186 ∗ 1 + 186 ∗ 2 + 186 ∗ 1 − 185 ∗ 1 − 186 ∗ 2 − 186 ∗ 1 = 1 G_{x2}=186*1 + 186*2 + 186*1 - 185*1- 186*2- 186*1=1 Gx2=1861+1862+1861185118621861=1

最后我们取他们的绝对值, 分别 187和1, 这里我们就很明显的看到了他们的区别,梯度值越大,说明该像素越有可能是边缘!

Shader 代码实现:

首先我们需要把图像置灰,置灰非常简单,通过灰度心理学公式 Gray = R*0.299 + G*0.587 + B*0.114 转换即可!

这里我们分别对横向和纵向做边缘检测,以便于我们做对比,理解。

创建一个C#脚本拖拽到摄像机上:
PS: PostEffectsBase 基类可以在我的这篇文章查看
或者到我的GitHub上获取

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;
	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 "lcl/learnShader3/002 Chapter 12/Edge Detection Test" {
	//-----------------------------【属性】-----------------------------
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
	}
	//-----------------------------【子着色器】-----------------------------
	SubShader {
		Pass {  
			ZTest Always Cull Off ZWrite Off
			
			CGPROGRAM
			
			#include "UnityCG.cginc"
			
			#pragma vertex vert  
			#pragma fragment frag
			
			sampler2D _MainTex;  
			uniform half4 _MainTex_TexelSize;
			
			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;
				//计算周围像素的纹理坐标位置,其中4为原始点,
				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.299 * color.r + 0.587 * color.g + 0.114 * color.b; 
			}
			
			// sobel算子
			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;
				for (int it = 0; it < 9; it++) {
					// 转换为灰度值
					texColor = luminance(tex2D(_MainTex, i.uv[it]));

					edgeX += texColor * Gx[it];
					// edgeY += texColor * Gy[it];
				}
				
				// half edge = 1 - abs(edgeX) - abs(edgeY);
				half edge = abs(edgeX);
				return edge;
			}
			//-----------------------------【片元着色器】-----------------------------
			fixed4 frag(v2f i) : SV_Target {
				half edge = Sobel(i);
				return edge;

 			}
			
			ENDCG
		} 
	}
	FallBack Off
}

最终结果对比
在这里插入图片描述
最后我们合并一下横向和纵向的边缘及原图,最终边缘检测结果为:

在这里插入图片描述
最终Shader代码:

//-----------------------------【边缘检测】-----------------------------
Shader "lcl/learnShader3/002 Chapter 12/Edge Detection Test" {
	//-----------------------------【属性】-----------------------------
	Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
		// 描边程度
		_EdgeOnly ("Edge Only", Float) = 1.0
		// 边缘颜色
		_EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
	}
	//-----------------------------【子着色器】-----------------------------
	SubShader {
		Pass {  
			ZTest Always Cull Off ZWrite Off
			
			CGPROGRAM
			
			#include "UnityCG.cginc"
			
			#pragma vertex vert  
			#pragma fragment frag
			
			sampler2D _MainTex;  
			uniform half4 _MainTex_TexelSize;
			fixed _EdgeOnly;
			fixed4 _EdgeColor;

			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;
				//计算周围像素的纹理坐标位置,其中4为原始点,
				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.299 * color.r + 0.587 * color.g + 0.114 * color.b; 
			}
			
			// sobel算子
			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;
				for (int it = 0; it < 9; it++) {
					// 转换为灰度值
					texColor = luminance(tex2D(_MainTex, i.uv[it]));

					edgeX += texColor * Gx[it];
					edgeY += texColor * Gy[it];
				}
				// 合并横向和纵向
				half edge = 1 - (abs(edgeX) + abs(edgeY));
				return edge;
			}
			//-----------------------------【片元着色器】-----------------------------
			fixed4 frag(v2f i) : SV_Target {
				half edge = Sobel(i);
				fixed4 edgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
				edgeColor = lerp(tex2D(_MainTex, i.uv[4]),edgeColor, _EdgeOnly);
				return edgeColor;
 			}
			
			ENDCG
		} 
	}
	FallBack Off
}

最后

有兴趣的小伙伴可以来我的GitHub上逛逛,里面有我学习Shader过程中的一些记录,实现的一些特效,喜欢的可以点个Star,嘿嘿,谢谢啦!

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 好的,下面是一个简单的用Unity Shader实现边缘发光效果的例子。 首先,我们需要一个称为"RenderTexture"的工具来在屏幕上渲染场景。这个RenderTexture将会成为我们在Shader中计算边缘发光的基础。 在这个Shader中,我们需要做以下几个步骤: 1. 将场景渲染到RenderTexture中。 2. 对RenderTexture进行处理,找到场景中的边缘,并将边缘标记为白色。 3. 对处理后的RenderTexture进行模糊处理,以产生发光效果。 4. 将模糊后的RenderTexture和原始场景图像混合在一起,产生最终的边缘发光效果。 下面是一个简单的Shader代码示例: ``` Shader "Custom/EdgeGlow" { Properties { _MainTex ("Texture", 2D) = "white" {} _EdgeColor ("Edge Color", Color) = (1,1,1,1) _EdgeWidth ("Edge Width", Range(0, 0.1)) = 0.01 _BlurRadius ("Blur Radius", Range(0, 10)) = 2 } SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float4 _EdgeColor; float _EdgeWidth; float _BlurRadius; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { // Step 1: Render scene to a RenderTexture float4 scene = tex2D(_MainTex, i.uv); // Step 2: Find edges and mark them white float4 edge = (4 * scene.rgba - tex2D(_MainTex, i.uv + float2(0, 1) * _MainTex_ST.y).rgba - tex2D(_MainTex, i.uv - float2(0, 1) * _MainTex_ST.y).rgba - tex2D(_MainTex, i.uv + float2(1, 0) * _MainTex_ST.x).rgba - tex2D(_MainTex, i.uv - float2(1, 0) * _MainTex_ST.x).rgba); edge = max(edge, 0); edge = step(_EdgeWidth, edge.r); // Step 3: Apply Gaussian blur to edges for (int i = -_BlurRadius; i <= _BlurRadius; i++) { for (int j = -_BlurRadius; j <= _BlurRadius; j++) { float2 offset = float2(i, j) * ### 回答2: 使用Unity Shader编写边缘发光效果可以通过以下几个步骤来实现: 1. 为对象创建一个新的材质,并在着色器选项中选择"Custom/Edge Glow"。 2. 编写一个新的边缘发光Shader,并将其与上一步骤中创建的材质相关联。 3. 在着色器中添加顶点和片段着色器函数。 4. 顶点着色器中,使用传入的模型视图矩阵和投影矩阵将顶点位置转换为屏幕空间坐标。 5. 片段着色器中,将屏幕空间坐标作为输入。 6. 在片段着色器中,使用采样函数获取当前像素的颜色。 7. 使用一个for循环,遍历像素的邻居,比较颜色差异,如果颜色差异超过一个阈值,则表示该像素位于边缘。 8. 将边缘像素的颜色设置为发光颜色,将非边缘像素的颜色设置为原始颜色。 9. 在Unity中将该材质应用到需要应用该边缘发光效果的对象上。 以上是一种实现边缘发光效果的例子,可以根据具体需求适应更多的应用场景和效果。编写Shader需要一定的Shader编程经验,建议在学习和实践的过程中参考Unity官方文档和其他相关资源。 ### 回答3: 要使用Unity Shader编写一段边缘发光效果,我们可以参考一种基本的方法。首先,我们需要两个Pass来实现效果:一个Pass用于将发光部分高亮,另一个Pass使用轮廓检测算法将边缘区域描绘出来。 首先,在顶点着色器中,我们需要将顶点位置从模型空间转换为剪辑空间,通过将顶点坐标乘以Unity内置变量`UNITY_MATRIX_MVP`实现。然后,我们可以将变换后的位置传递给片段着色器。 在片段着色器中,我们需要进行两个Pass。在第一个Pass中,通过计算法线和视线的角度余弦值,将发光部分高亮。我们可以使用光照信息和噪声函数来模拟高亮效果。最后,我们将高亮部分的颜色与原始颜色进行混合。 在第二个Pass中,我们使用轮廓检测算法来描绘边缘。我们可以通过计算像素的法线差异来确定边缘区域。如果法线差异大于阈值,则将像素颜色设置为边缘颜色,否则保持原始颜色。 边缘发光效果的实现需要额外的几何信息,例如模型的法线信息。因此,在Unity中将需要在材质中导入法线贴图,并在Shader中进行采样。 总结起来,要使用Unity Shader编写一段边缘发光效果,我们需要进行两个Pass:一个用于高亮发光部分,另一个用于描绘边缘部分。通过计算角度余弦和法线差异,我们可以实现边缘发光效果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值