【Unity3D】基于深度和法线纹理的边缘检测方法
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 设置为白色

声明:本文转自【Unity3D】基于深度和法线纹理的边缘检测方法。
【Unity3D】基于深度和法线纹理的边缘检测方法的更多相关文章
- Unity Shader入门精要学习笔记 - 第13章 使用深度和法线纹理
线纹理的代码非常简单,但是我们有必要在这之前首先了解它们背后的实现原理. 深度纹理实际上就是一张渲染纹理,只不过它里面存储的像素值不是颜色值而是一个高精度的深度值.由于被存储在一张纹理中,深度纹理里的 ...
- 《Unity shader入门精要》复习<第13章 关于NDC坐标和深度/法线纹理>
分为三个地方讲解. NDC(Normalize Device Coordinates)归一化的设备坐标 NDC坐标是世界空间坐标通过MVP变换之后再进行归一化得到的坐标.只需要再一步变换就能得到屏幕空 ...
- 【Unity Shaders】法线纹理(Normal Mapping)的实现细节
写在前面 写这篇的目的是为了总结我长期以来的混乱.虽然题目是"法线纹理的实现细节",但其实我想讲的是如何在shader中编程正确使用法线进行光照计算.这里面最让人头大的就是各种矩阵 ...
- 腾讯优图&港科大提出一种基于深度学习的非光流 HDR 成像方法
目前最好的高动态范围(HDR)成像方法通常是先利用光流将输入图像对齐,随后再合成 HDR 图像.然而由于输入图像存在遮挡和较大运动,这种方法生成的图像仍然有很多缺陷.最近,腾讯优图和香港科技大学的研究 ...
- #Deep Learning回顾#之基于深度学习的目标检测(阅读小结)
原文链接:https://www.52ml.net/20287.html 这篇博文主要讲了深度学习在目标检测中的发展. 博文首先介绍了传统的目标检测算法过程: 传统的目标检测一般使用滑动窗口的框架,主 ...
- Unity3d 基于物理渲染Physically-Based Rendering之最终篇
前情提要: 讲求基本算法 Unity3d 基于物理渲染Physically-Based Rendering之specular BRDF plus篇 Unity3d 基于物理渲染Physically-B ...
- 【OCR技术系列之四】基于深度学习的文字识别(3755个汉字)
上一篇提到文字数据集的合成,现在我们手头上已经得到了3755个汉字(一级字库)的印刷体图像数据集,我们可以利用它们进行接下来的3755个汉字的识别系统的搭建.用深度学习做文字识别,用的网络当然是CNN ...
- OCR技术浅探:基于深度学习和语言模型的印刷文字OCR系统
作者: 苏剑林 系列博文: 科学空间 OCR技术浅探:1. 全文简述 OCR技术浅探:2. 背景与假设 OCR技术浅探:3. 特征提取(1) OCR技术浅探:3. 特征提取(2) OCR技术浅探:4. ...
- 基于深度学习的目标检测技术演进:R-CNN、Fast R-CNN、Faster R-CNN
object detection我的理解,就是在给定的图片中精确找到物体所在位置,并标注出物体的类别.object detection要解决的问题就是物体在哪里,是什么这整个流程的问题.然而,这个问题 ...
- 深度学习与计算机视觉(12)_tensorflow实现基于深度学习的图像补全
深度学习与计算机视觉(12)_tensorflow实现基于深度学习的图像补全 原文地址:Image Completion with Deep Learning in TensorFlow by Bra ...
随机推荐
- 【MCU】浮点数如何判等
[来源]https://mp.weixin.qq.com/s/481H4imm73IIS1yFI7-DNA
- [java] - JavaBeans 获取 session
RegServlet // 保存到 session request.getSession().setAttribute("user", user); userinfo.jsp // ...
- Hexo中引入另一个文件内容
有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址 安装插件 npm install hexo-include-m ...
- [转帖]TiDB系统调参实战经验
https://tidb.net/blog/c9466c40#TiDB%E7%B3%BB%E7%BB%9F%E8%B0%83%E5%8F%82%E5%AE%9E%E6%88%98%E7%BB%8F%E ...
- [转帖]Shell三剑客之awk
目录 awk简述 awk的工作过程 awk的工作原理 awk的基本格式及其内置变量 getline 文本内容匹配过滤打印 对字段进行处理打印 条件判断打印 awk的三元表达式与精准筛选用法 awk的精 ...
- [转帖]带你重走 TiDB TPS 提升 1000 倍的性能优化之旅
https://tidb.net/blog/29074d86#TiDB%20%E6%80%A7%E8%83%BD%E5%92%8C%E7%A8%B3%E5%AE%9A%E6%80%A7%E7%9A%8 ...
- [转帖]jmeter正则表达式提取器获取数组数据-02篇
接上篇,当我们正则表达式匹配到多个值以后,入下图所示,匹配到21个结果,如果我们想一次拿到这一组数据怎么办呢 打开正则表达式提取器页面,匹配数字填入-1即可 通过调试取样器就可以看到匹配到已经匹配到多 ...
- [转帖]redis 持久化方式 - aof 和 rdb 区别
https://wenfh2020.com/2020/04/01/redis-persistence-diff/ aof 和 rdb 是 redis 持久化的两种方式.我们看看它们的特点和具体应用 ...
- 记一次JSF异步调用引起的接口可用率降低
前言 本文记录了由于JSF异步调用超时引起的接口可用率降低问题的排查过程,主要介绍了排查思路和JSF异步调用的流程,希望可以帮助大家了解JSF的异步调用原理以及提供一些问题排查思路.本文分析的JSF源 ...
- NutUI-React 京东移动端组件库 2月份上新!欢迎使用!
作者:京东零售 佟恩 NutUI 是一款京东风格的移动端组件库.NutUI 目前支持 Vue 和 React技术栈,支持Taro多端适配. 本次,是2月的一个示例输出,希望对你有帮助! 2月,我们对组 ...