【Unity3D】基于深度和法线纹理的边缘检测方法

发布时间 2023-08-12 23:03:42作者: little_fat_sheep

1 前言

边缘检测特效中使用屏后处理技术,通过卷积运算计算梯度,检测每个像素周围像素的亮度差异,以识别是否是边缘像素;选中物体描边特效中也使用了屏后处理技术,通过 CommandBuffer 获取目标物体渲染后的模板纹理,并将模板纹理模糊化,使得模板纹理的边缘向外扩张,再将模糊化后的模板纹理与原纹理进行合成,得到描边后的纹理;基于模板测试和顶点膨胀的描边方法中开 2 个 Pass 渲染通道,第一个 Pass 通道将待描边物体的屏幕区域像素对应的模板值标记为 1,第二个 Pass 通道将待描边物体的顶点向外膨胀,绘制模板值为非 1 的膨胀区域(即外环区域),实现描边。

边缘检测特效边缘检测特效中容易将物体的影子等误识别为边缘,选中物体描边特效基于模板测试和顶点膨胀的描边方法中存在层叠消融现象。本文将实现另一张边缘检测算法:基于深度和法线纹理的边缘检测算法。

​ 本文完整资源见→Unity3D基于深度和法线纹理的边缘检测方法

2 边缘检测原理

​ 对渲染后的屏幕纹理进行二次渲染,根据像素点周围深度和法线的差异判断是否是物体边缘像素,如果是边缘,需要重新进行边缘着色。判断边缘的具体做法是:对该像素点周围的像素点的深度和法线进行卷积运算,得到该点的梯度(反映该点附近深度和法线突变的强度),根据梯度阈值判断该点是否是边缘。 深度和法线纹理介绍见→屏幕深度和法线纹理简介

1)边缘检测算子

​ 边缘检测算子和梯度的定义见→边缘检测特效。本文将使用 Roberts 边缘检测算子,如下:

img

​ 使用 1 范式梯度,如下:

img

2)深度差异计算

​ 深度差异计算如下,depth1、depth2 分别为待检测点左上和右下(或右上和左下)像素点的深度值,_DepthScale 为深度缩放系数。

float depthDelta = abs(depth1 - depth2) * depth1 * _DepthScale; // 深度差异(距离相机越远, 像素点越少, 对深度差越敏感, 所以乘了depth1)

3)法线差异计算

​ 法线差异计算如下,normal1、normal2 分别为待检测点左上和右下(或右上和左下)像素点的法线向量(已归一化),_NormalScale 为法线缩放系数。

float normalDelta = (1 - abs(dot(normal1, normal2))) * _NormalScale; // 法线差异

3 边缘检测实现

​ EdgeDetection.cs

using UnityEngine;

[RequireComponent(typeof(Camera))] // 需要相机组件
public class EdgeDetection : MonoBehaviour {
	[Range(0.0f, 1.0f)]
	public float edgesOnly = 0.0f; // 是否仅显示边缘
	public Color edgeColor = Color.black; // 边缘颜色
	public Color backgroundColor = Color.white; // 背景颜色
	public float sampleScale = 1.0f; // 采样缩放系数(值越大, 描边越宽)
	public float depthScale = 1.0f; // 深度缩放系数(值越大, 越易识别为边缘)
	public float normalScale = 1.0f; // 法线缩放系数(值越大, 越易识别为边缘)

	private Material material; // 材质

	private void Start() {
		material = new Material(Shader.Find("MyShader/EdgeDetect"));
		material.hideFlags = HideFlags.DontSave;
	}

	private void OnEnable() {
		GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
	}

	//[ImageEffectOpaque] // 在不透明的 Pass 执行完毕后立即调用 OnRenderImage 方法(透明物体不需要描边)
	private void OnRenderImage(RenderTexture src, RenderTexture dest) {
		if (material != null) {
			material.SetFloat("_EdgeOnly", edgesOnly);
			material.SetColor("_EdgeColor", edgeColor);
			material.SetColor("_BackgroundColor", backgroundColor);
			material.SetFloat("_SampleScale", sampleScale);
			material.SetFloat("_DepthScale", depthScale);
			material.SetFloat("_NormalScale", normalScale);
			Graphics.Blit(src, dest, material);
		} else {
			Graphics.Blit(src, dest);
		}
	}
}

​ EdgeDetection.shader

Shader "MyShader/EdgeDetect" {
	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) // 背景颜色
		_SampleScale("Sample Scale", Float) = 1.0 // 采样缩放系数(值越大, 描边越宽)
		_DepthScale("Depth Scale", Float) = 1.0 // 深度缩放系数(值越大, 越易识别为边缘)
		_NormalScale("Normal Scale", Float) = 1.0 // 法线缩放系数(值越大, 越易识别为边缘)
	}

	SubShader {
		Pass {
			// 深度测试始终通过, 关闭深度写入
			ZTest Always ZWrite Off

			CGPROGRAM
			
			#include "UnityCG.cginc"
			
			#pragma vertex vert  
			#pragma fragment frag
			
			sampler2D _MainTex; // 主纹理
			sampler2D _CameraDepthNormalsTexture; // 深度&法线纹理
			uniform half4 _MainTex_TexelSize;  // _MainTex的像素尺寸大小, float4(1/width, 1/height, width, height)
			fixed _EdgeOnly; // 是否仅显示边缘
			fixed4 _EdgeColor; // 边缘颜色
			fixed4 _BackgroundColor; // 背景颜色
			float _SampleScale; // 采样缩放系数(值越大, 描边越宽)
			float _DepthScale; // 深度缩放系数(值越大, 越易识别为边缘)
			float _NormalScale; // 法线缩放系数(值越大, 越易识别为边缘)

			struct v2f {
				float4 pos : SV_POSITION; // 裁剪空间中顶点坐标
				half2 uv[5] : TEXCOORD0; // 顶点及其周围4个角的uv坐标
			};

			v2f vert(appdata_img v) {
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.uv[0] = v.texcoord;
				o.uv[1] = v.texcoord + _MainTex_TexelSize.xy * half2(1, 1) * _SampleScale;
				o.uv[2] = v.texcoord + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleScale;
				o.uv[3] = v.texcoord + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleScale;
				o.uv[4] = v.texcoord + _MainTex_TexelSize.xy * half2(1, -1) * _SampleScale;
				return o;
			}
			
			bool isEdge(half4 sample1, half4 sample2) { // 是否是边缘(1: 是, 0: 否)
				// 计算深度差
				float depth1 = DecodeFloatRG(sample1.zw); // 观察空间中的线性且归一化的深度
				float depth2 = DecodeFloatRG(sample2.zw); // 观察空间中的线性且归一化的深度
				float depthDelta = abs(depth1 - depth2) * depth1 * _DepthScale; // 深度差异(距离相机越远, 像素点越少, 对深度差越敏感, 所以乘了depth1)
				bool isDepthDiff = depthDelta > 0.01; // 深度是否不同
				// 计算法线差
				float3 normal1 = DecodeViewNormalStereo(sample1); // 观察空间中的法线向量
				float3 normal2 = DecodeViewNormalStereo(sample2); // 观察空间中的法线向量
				float normalDelta = (1 - abs(dot(normal1, normal2))) * _NormalScale; // 法线差异
				bool isNormalDiff = normalDelta > 0.14; // cos(30°)=0.86, 法线夹角小于30°
				return isDepthDiff || isNormalDiff;
			}

			fixed4 frag(v2f i) : SV_Target {
				half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
				half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
				half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
				half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
				bool isDiff = isEdge(sample1, sample2); // 是否是边缘
				isDiff = isDiff && isEdge(sample3, sample4);
				if (isDiff) {
					return _EdgeColor;
				}
				fixed4 tex = tex2D(_MainTex, i.uv[0]);
				return lerp(tex, _BackgroundColor, _EdgeOnly);
 			}
			
			ENDCG
		} 
	}

	FallBack Off
}

3 运行效果

1)原图

img

2) Edges Only 设置为 0,Edge Color 设置为绿色

img

3)Edges Only 设置为 1,Edge Color 设置为黑色,Background Color 设置为白色

img

​ 声明:本文转自【Unity3D】基于深度和法线纹理的边缘检测方法