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

news2024/9/20 11:01:57

1 前言

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

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

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

2 边缘检测原理

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

        1)边缘检测算子

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

         使用 1 范式梯度,如下:

         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)原图

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/674437.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

学习spring: 1.引子

问题 我们来看一段代码: package org.malred;import org.malred.entity.Car; import org.malred.entity.Tire;/*** Hello world!**/ public class App {public static void main(String[] args) {Car car new Car();Tire lt new Tire();car.setLeftTire(lt);Tire rt new T…

状态估计器

文章目录 [toc] 1.状态空间模型1.1.连续状态空间模型1.2.离散状态空间模型 2.矩阵微积分3.二次规划4.概率论4.1.期望与方差4.2.独立事件4.3.向量随机变量4.4.噪声与协方差矩阵4.5.条件概率 5.最小二乘估计5.1.加权最小二乘估计5.2.递推最小二乘估计5.3.状态向量和协方差随时间变…

深入理解WebSocket,让你入门音视频

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。&#x1f60a; 座右铭&#xff1a;不想…

Android 使用okhttp监控网络数据

这里使用Okhttp写了一个demo来监听网络请求过程中的一系列数据&#xff0c;包括当前网络类型、请求体、响应体大小&#xff0c;url&#xff0c;请求方式&#xff0c;当然还有本次核心获取域名解析时长&#xff0c;建立连接时长&#xff0c;保持连接时长&#xff0c;请求总时长这…

《C++ Primer》--学习6

IO库 IO类 为了支持使用宽字符的语言&#xff0c;标准库定义了一组类型和对象来操纵 wchar_t 类型的数据。宽字符版本的类型和函数的名字以一个 w 开始。wcin wcout 和 wcerr 是分别对应 cin cout 和 cerr 的宽字符版本对象 IO类型之间的关系 类型 ifstream 和 istringstream…

Vuex 状态管理 —— 核心store

在上一篇当中讲到关于接口请求函数获取数据&#xff0c;拿到 response.data &#xff0c;简化调用&#xff0c;那么在拿到请求的响应数据之后呢&#xff1f;在前面讲到组件间的通信当中&#xff0c;如父子通信(父传子props,子传父$emit)以及组件与组件之前不能通过直接通信&…

【33】用 Docker 部署 Prometheus + Grafana 监控平台

一. Docker部署Prometheus 1.1 下载prom/prometheus镜像 docker pull prom/prometheus 1.2 启动prometheus容器 docker run -itd --nameprometheus -p 9090:9090 prom/prometheus 打开本地http://localhost:9090/ 说明启动成功 1.3 将容器的配置文件复制出来 docker cp pr…

深入理解深度学习——GPT(Generative Pre-Trained Transformer):在不同任务中使用GPT

分类目录&#xff1a;《自然语言处理从入门到应用》总目录 GPT预训练语言模型作为一个标准的语言模型&#xff0c;其输入和输出是固定的&#xff0c;即输入一个词序列&#xff0c;输出该词序列的下一个词。《深入理解深度学习——GPT&#xff08;Generative Pre-Trained Transf…

GAMES101 笔记 Lecture06 Rasterization2(Antialiasing and Z-Buffering)

目录 Antialiasing(反走样)Sampling is Ubiquitous in Computer Graphics(采样在计算机图形学中无处不在)Sampling Artifacts(Errors or Mistakes or Inaccuracies) in Computer Graphics(在计算机图形学中采样的瑕疵)Jaggies(Staircase Pattern)锯齿Moire Pattern in Imaging(…

[进阶]TCP通信实现BS架构,网站开发的原理,线程池优化BS架构

代码演示如下&#xff1a; 服务端 public class Server {public static void main(String[] args) throws Exception{System.out.println("服务端开启&#xff01;");//1.创建ServerSocket的对象&#xff0c;同时为服务端注册端口。ServerSocket serverSocket new…

Wang tile(王浩瓷砖)算法解决贴图平铺重复问题

Wang tile(王浩瓷砖) 大家好&#xff0c;我是阿赵。这次来解决一个贴图重复的问题。 一、问题 做一篇很大面积的草地&#xff0c;一般思路是建立一个地面的面片&#xff0c;然后在材质球里面给他做一个Tiling平铺&#xff0c;增大重复次数。这样整个地面都可以被草地的贴图铺满…

Spring Boot 如何使用 @Validated 注解进行数据校验

Spring Boot 如何使用 Validated 注解进行数据校验 在开发应用程序时&#xff0c;数据校验通常是不可避免的。Spring Boot 提供了许多选项来验证应用程序中的数据&#xff0c;其中一个选项是使用 Validated 注解。本文将介绍如何使用 Validated 注解进行数据校验&#xff0c;并…

操作系统-操作系统结构

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

【计算机组成原理】Yy-z02硬布线模型机设计

目录 一、Yy-z02模型机的系统结构 二、Yy-z02模型机的数据通路 三、Yy-z02模型机的指令执行 四、Yy-z02模型机的硬布线控制器 一、Yy-z02模型机的系统结构 指令系统的实现 <--- 构造它的硬件系统 硬件系统构造过程&#xff1a; 分析指令格式和各指令的功能确定部件连…

《机器学习公式推导与代码实现》chapter16-集成学习对比与调参

《机器学习公式推导与代码实现》学习笔记&#xff0c;记录一下自己的学习过程&#xff0c;详细的内容请大家购买作者的书籍查阅。 集成学习&#xff1a;对比与调参 虽然现在深度学习大行其道&#xff0c;但以XGBoost、LightGBM、CatBoost为代表的Boosting算法仍有其广泛的用武…

【Applied Algebra】有限状态机和模型检测初探

【Applied Algebra】有限状态机和模型检测初探 摘要:有限状态机(FSM)和模型检测有密切的联系。有限状态机提供了一种用状态转换图来表示系统行为的简单方法。而模型检测是一种针对形式化模型&#xff08;例如有限状态机&#xff09;的验证技术&#xff0c;旨在自动验证模型是否…

css基础(一)

目录 思维导图 ​一、css简介 1.1 css语法规范 1.2 css代码规格 1. 样式格式书写 2. 样式大小写 3. 空格规范 二、css选择器 2.1 CSS 选择器的作用 2.2 选择器分类 2.3 标签选择器 2.4 类选择器 2.4 类选择器-多类名 2.5 id 选择器 2.6 通配符选择器 2.7 基础选择器总结 三、CS…

D. Running Miles(公式转换)

Problem - D - Codeforces 有一条长为n的街道&#xff0c;其中第i个景点距离街道起点i英里。第i个景点的美丽值为bi。你想要在离街道起点l英里和r英里处开始和结束慢跑。当你跑步时&#xff0c;你会看到你经过的景点&#xff08;包括起点和终点处的景点&#xff09;。你对沿途慢…

Microsoft365有用吗?2023最新版office有哪些新功能?

office自97版到现在已有20多年&#xff0c;一直是作为行业标准&#xff0c;格式和兼容性好&#xff0c;比较正式&#xff0c;适合商务使用。包含多个组件&#xff0c;除了常用的word、excel、ppt外&#xff0c;还有收发邮件的outlook、管理数据库的access、排版桌面的publisher…

CENTOS上的网络安全工具(二十五)SPARK+NetSA Security Tools容器化部署(1)

一、第三代YAF YAF&#xff08;Yet Another Flowmeter&#xff09;是作为CERT NetSA安全工具套件的传感器部分存在的&#xff0c;支持输入实时数据流和PCAP文件&#xff0c;解析并输出流数据&#xff0c;或针对特定协议的深包检测元数据。目前&#xff0c;YAF在整个系统的作用如…