80.深度和法线纹理-效果实现-深度法线纹理实现边缘检测-具体实现
80.1 知识点
知识回顾:深度+法线纹理实现边缘检测的基本原理
- 基于 Roberts (罗伯茨) 交叉算子,通过比较对角线上的像素的深度和法线值,判断是否处于边缘上。
- 步骤1:获取对角线像素
利用纹素变量进行 UV 坐标偏移,得到对角线的像素位置。可以通过采样偏移距离变量控制描边的粗细。 - 步骤2:进行深度和法线值的比较
获取对角线像素的深度和法线信息,通过减法得到差值,用差值和阈值进行比较,判断两点是否在同一平面上。 - 步骤3:判断是否在边缘
根据比较结果,决定中心像素使用的是原本的颜色还是描边的颜色。
实现 利用深度+法线纹理实现边缘检测屏幕后期处理效果 对应 Shader
主要步骤
新建Shader
- 取名
EdgeDetectionWithDepthNormalsTexture
- 删除无用代码。
- 取名
声明属性,属性映射
- 主纹理:
_MainTex
- 边缘检测强度:
_EdgeOnly
(0 显示场景,1 只显示边缘,用于控制自定义背景色的程度) - 描边颜色:
_EdgeColor
- 背景颜色:
_BackgroundColor
- 采样偏移距离:
_SampleDistance
- 深度敏感度:
_SensitivityDepth
- 法线敏感度:
_SensitivityNormal
- 注意:映射属性时需要加入:
- 纹素:
_MainTex_TexelSize
- 深度 + 法线纹理:
_CameraDepthNormalsTexture
- 纹素:
- 主纹理:
屏幕后处理标配
结构体
- 顶点坐标
- uv数组,包含5个空间(存储中心点、对角线四个点)
顶点着色器
- 顶点坐标转换
- 5个uv坐标赋值,按以下顺序:
- 中心点、左上角、右下角、右上角、左下角
片元着色器
- 直接采样得到对角线四个点的深度法线信息
- 实现一个用于比较两点深度、法线信息的函数,返回0或1,方便进行插值计算
- 声明一个插值变量,0代表使用边缘色,1代表使用源颜色
- 考虑背景色差值
FallBack Off
新建Shader,取名EdgeDetectionWithDepthNormalsTexture,删除无用代码
Shader "Unlit/Lesson80_EdgeDetectionWithDepthNormalsTexture"
{
Properties
{
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
ENDCG
}
}
}
声明属性,并进行属性映射,包括主纹理、边缘检测强度(0显示场景 1只显示边缘 用于控制自定义背景色程度—)、描边颜色、背景颜色、采样偏移距离、深度敏感度、法线敏感度、纹素、深度+法线纹理 _CameraDepthNormalsTexture(定死的名称)
Properties
{
//主纹理
_MainTex ("Texture", 2D) = "white" {}
//用于控制自定义背景颜色程度的 0要显示原始背景色 1只显示边缘 完全显示自定义背景色
_EdgeOnly("EdgeOnly", Float) = 0
//边缘的描边颜色
_EdgeColor("EdgeColor", Color) = (0,0,0,0)
//自定义背景颜色
_BackgroundColor("BackgroundColor", Color) = (1,1,1,1)
//采样偏移程度 主要用来控制描边的粗细 值越大越粗 反之越细
_SampleDistance("SampleDistance", Float) = 1
//深度和法线的敏感度 用来进行这个差值判断时 起作用
_SensitivityDepth("SensitivityDepth", Float) = 1
_SensitivityNormal("SensitivityNormal", Float) = 1
}
//主纹理
sampler2D _MainTex;
//纹素 用于进行uv坐标偏移 取得周围像素的uv坐标的
half4 _MainTex_TexelSize;
//深度+法线纹理
sampler2D _CameraDepthNormalsTexture;\
//用于控制自定义背景颜色程度的 0要显示原始背景色 1只显示边缘 完全显示自定义背景色
fixed _EdgeOnly;
//边缘的描边颜色
fixed4 _EdgeColor;
//自定义背景颜色
fixed4 _BackgroundColor;
//采样偏移程度 主要用来控制描边的粗细 值越大越粗 反之越细
float _SampleDistance;
//深度和法线的敏感度 用来进行这个差值判断时 起作用
float _SensitivityDepth;
float _SensitivityNormal;
声明屏幕后处理标配指令
//屏幕后处理标配
ZTest Always // 始终通过深度测试
Cull Off // 关闭背面剔除
ZWrite Off // 禁止写入深度缓冲
声明结构体,包括顶点坐标和uv数组(5个空间,存储中心点、对角线4个点)
struct v2f
{
//用于存储5个像素的uv坐标
half2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
顶点函数进行顶点坐标转换和5个uv坐标赋值
//顶点着色器
v2f vert(appdata_base appdata_base)
{
v2f v2f;
//转换到裁剪空间
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
//uv相关坐标赋值
half2 uv = appdata_base.texcoord;
//中心点
v2f.uv[0] = uv;
//对角线相关像素uv坐标赋值
//左上角
v2f.uv[1] = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance;
//右下角
v2f.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance;
//右上角
v2f.uv[3] = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance;
//左下角
v2f.uv[4] = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance;
return v2f;
}
实现一个用于比较两点深度、法线信息的函数,返回0或1,方便进行插值计算
//用于比较两个点的深度和法线纹理中采样得到的信息 用来判断是否是边缘
//返回值的含义
//1 - 法线和深度值基本相同 处于同一个平面上
//0 - 差异大 不在一个平面上
half CheckSame(half4 depthNormal1, half4 depthNormal2)
{
//分别得到两个点信息的深度和法线
//第一个点的
//得到深度值
float depth1 = DecodeFloatRG(depthNormal1.zw);
//得到法线的xy
float2 normal1 = depthNormal1.xy;
//第二个点的
//得到深度值
float depth2 = DecodeFloatRG(depthNormal2.zw);
//得到法线的xy
float2 normal2 = depthNormal2.xy;
//法线的差异计算
//计算两条法线的xy的差值 并且乘以 自定义的敏感度
float2 normalDiff = abs(normal1 - normal2) * _SensitivityNormal;
//判断两个法线是否在一个平面
//如果差异不大 证明基本上在一个平面上 返回 1;否则返回0
int isSameNormal = (normalDiff.x + normalDiff.y) < 0.1;
//深度的差异计算
float depthDiff = abs(depth1 - depth2) * _SensitivityDepth;
//判断深度是不是很接近 是不是相当于在一个平面上
//如果满足条件 证明深度值差异非常小 基本趋近于在一个平面上 返回1;否则 返回0
int isSameDepth = depthDiff < 0.1 * depth1;
//返回值的含义
//1 - 法线和深度值基本相同 处于同一个平面上
//0 - 差异大 不在一个平面上
return isSameDepth * isSameNormal ? 1 : 0;
}
片元函数直接采样得到对角线四个点的深度法线信息,调用比较函数得到是否是边缘,考虑背景色进行三次插值
//片元着色器
fixed4 frag(v2f v2f) : SV_Target
{
// 从深度+法线纹理中采样四个邻近点的深度和法线信息
// 左上角像素的深度和法线信息
half4 TL = tex2D(_CameraDepthNormalsTexture, v2f.uv[1]);
// 右下角像素的深度和法线信息
half4 BR = tex2D(_CameraDepthNormalsTexture, v2f.uv[2]);
// 右上角像素的深度和法线信息
half4 TR = tex2D(_CameraDepthNormalsTexture, v2f.uv[3]);
// 左下角像素的深度和法线信息
half4 BL = tex2D(_CameraDepthNormalsTexture, v2f.uv[4]);
// 初始化边缘插值值,默认设为 1(表示未检测到边缘)
half edgeLerpValue = 1;
// 比较对角线上两个点的深度和法线信息
// 如果两个点深度和法线的差异较大(即不在同一平面),edgeLerpValue 会被置为 0
edgeLerpValue *= CheckSame(TL, BR); // 左上和右下
edgeLerpValue *= CheckSame(TR, BL); // 右上和左下
// 根据检测到的边缘信息对颜色进行插值
// 根据是否是边缘,混合边缘颜色 (_EdgeColor) 和场景原始颜色 (tex2D(_MainTex, v2f.uv[0]))
// 如果 edgeLerpValue 为 1,显示场景颜色;如果为 0,显示边缘颜色
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, v2f.uv[0]), edgeLerpValue);
// 根据是否是边缘,边缘颜色与自定义背景颜色 (_BackgroundColor) 进行插值
// edgeLerpValue 为 1 时显示背景颜色;为 0 时显示边缘颜色
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edgeLerpValue);
// 将计算后的混合原纹理颜色和混合自定义背景颜色插值
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
不使用回退 Shader
Fallback Off// 不使用回退Shader
实现 利用深度+法线纹理实现边缘检测屏幕后期处理效果 对应 C#
主要步骤
新建C#脚本
- 取名与Shader相同。
继承
PostEffectBase
声明可控变量
Start函数
- 开启深度 + 法线纹理
- 注意:使用
|=
操作符,而不是=
,避免关闭深度纹理。
重写
UpdateProperty
函数- 在其中设置属性即可。
新建C#脚本,取名和Shader相同,继承PostEffectBase。Start函数中 开启深度+法线纹理,注意不要直接=,使用|=,避免关闭深度纹理.声明Shader中的可变变量,重写UpdateProperty函数,在其中设置属性
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 自定义的屏幕后处理效果,用于基于深度和法线纹理的边缘检测
public class Lesson80_EdgeDetectionWithDepthNormalsTexture : PostEffectBase
{
// 边缘显示程度,范围 [0, 1]
// 0 表示显示完整的原始场景,1 表示只显示边缘
[Range(0, 1)] public float edgeOnly = 0;
// 边缘的描边颜色
public Color edgeColor = Color.black;
// 背景的自定义颜色
public Color backgroundColor = Color.white;
// 采样偏移量,用于控制边缘粗细
public float sampleDistance = 1;
// 对深度敏感程度,用于判断是否是边缘
public float sensitivityDepth = 1;
// 对法线敏感程度,用于判断是否是边缘
public float sensitivityNormal = 1;
void Start()
{
// 确保摄像机启用深度+法线纹理模式,以便采样深度和法线信息
// 使用按位或操作防止覆盖其他后处理需求
Camera.main.depthTextureMode |= DepthTextureMode.DepthNormals;
}
// 更新材质属性的方法,由基类调用
protected override void UpdateProperty()
{
// 如果材质存在,更新其对应的参数值
if (material != null)
{
// 设置边缘显示程度
material.SetFloat("_EdgeOnly", edgeOnly);
// 设置边缘颜色
material.SetColor("_EdgeColor", edgeColor);
// 设置背景颜色
material.SetColor("_BackgroundColor", backgroundColor);
// 设置采样偏移量(边缘粗细)
material.SetFloat("_SampleDistance", sampleDistance);
// 设置深度敏感度
material.SetFloat("_SensitivityDepth", sensitivityDepth);
// 设置法线敏感度
material.SetFloat("_SensitivityNormal", sensitivityNormal);
}
}
}
挂载到摄像机关联shader,修改参数可以查看边缘效果
80.2 知识点代码
Lesson80_深度和法线纹理_效果实现_深度法线纹理实现边缘检测_具体实现.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Lesson80_深度和法线纹理_效果实现_深度法线纹理实现边缘检测_具体实现 : MonoBehaviour
{
void Start()
{
#region 知识回顾 深度+法线纹理实现边缘检测 基本原理
//基于Roberts(罗伯茨)交叉算子,通过比较对角线上的的像素的深度和法线值,判断是否在边缘上。
//1.得到对角线上的像素
// 利用纹素变量进行uv坐标偏移,得到对角线像素位置(可以加入一个采样偏移距离变量控制描边粗细)
//2.进行深度和法线值的比较
// 获取对角线像素的深度+法线信息,通过减法得到差值,用差值和阈值进行比较,判断两点是否在同一平面上
//3.决定是否在边缘
// 根据比较结果,决定中心像素使用的颜色是原本的颜色还是描边的颜色
#endregion
#region 知识点一 实现 利用深度+法线纹理实现边缘检测屏幕后期处理效果 对应 Shader
//1.新建Shader,取名EdgeDetectionWithDepthNormalsTexture
// 删除无用代码
//2.声明属性,属性映射
// 主纹理 _MainTex
// 边缘检测强度 _EdgeOnly 0显示场景 1只显示边缘 用于控制自定义背景色程度
// 描边颜色 _EdgeColor
// 背景颜色 _BackgroundColor
// 采样偏移距离 _SampleDistance
// 深度敏感度 _SensitivityDepth
// 法线敏感度 _SensitivityNormal
// 注意:映射属性时需要加入
// 纹素 _MainTex_TexelSize
// 深度+法线纹理 _CameraDepthNormalsTexture
//3.屏幕后处理标配
//4.结构体
// 顶点坐标
// uv数组,5个空间(存储中心点、对角线4个点)
//5.顶点着色器
// 5-1.顶点坐标转换
// 5-2.5个uv坐标赋值,按这样的顺序
// 中心点、左上角、右下角、右上角、左下角
//6.片元着色器
// 6-1.直接采样得到对角线四个点的深度法线信息
// 6-2.实现一个用于比较两点深度、法线信息的函数,返回0或1,方便进行插值计算
// 6-3.声明一个插值变量 0代表使用边缘色 1代表使用源颜色
// 6-4.考虑背景色差值
//7.FallBack Off
#endregion
#region 知识点二 实现 利用深度+法线纹理实现边缘检测屏幕后期处理效果 对应 C#
//1.新建C#脚本,取名和Shader相同
//2.继承PostEffectBase
//3.声明可控变量
//4.Start函数中 开启深度+法线纹理
// 注意:不要直接=,要|=,避免关闭深度纹理
//5.重写UpdateProperty函数
// 在其中设置属性即可
#endregion
}
}
Lesson80_EdgeDetectionWithDepthNormalsTexture.shader
Shader "Unlit/Lesson80_EdgeDetectionWithDepthNormalsTexture"
{
Properties
{
//主纹理
_MainTex ("Texture", 2D) = "white" {}
//用于控制自定义背景颜色程度的 0要显示原始背景色 1只显示边缘 完全显示自定义背景色
_EdgeOnly("EdgeOnly", Float) = 0
//边缘的描边颜色
_EdgeColor("EdgeColor", Color) = (0,0,0,0)
//自定义背景颜色
_BackgroundColor("BackgroundColor", Color) = (1,1,1,1)
//采样偏移程度 主要用来控制描边的粗细 值越大越粗 反之越细
_SampleDistance("SampleDistance", Float) = 1
//深度和法线的敏感度 用来进行这个差值判断时 起作用
_SensitivityDepth("SensitivityDepth", Float) = 1
_SensitivityNormal("SensitivityNormal", Float) = 1
}
SubShader
{
//屏幕后处理标配
ZTest Always // 始终通过深度测试
Cull Off // 关闭背面剔除
ZWrite Off // 禁止写入深度缓冲
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
//主纹理
sampler2D _MainTex;
//纹素 用于进行uv坐标偏移 取得周围像素的uv坐标的
half4 _MainTex_TexelSize;
//深度+法线纹理
sampler2D _CameraDepthNormalsTexture;\
//用于控制自定义背景颜色程度的 0要显示原始背景色 1只显示边缘 完全显示自定义背景色
fixed _EdgeOnly;
//边缘的描边颜色
fixed4 _EdgeColor;
//自定义背景颜色
fixed4 _BackgroundColor;
//采样偏移程度 主要用来控制描边的粗细 值越大越粗 反之越细
float _SampleDistance;
//深度和法线的敏感度 用来进行这个差值判断时 起作用
float _SensitivityDepth;
float _SensitivityNormal;
struct v2f
{
//用于存储5个像素的uv坐标
half2 uv[5] : TEXCOORD0;
float4 vertex : SV_POSITION;
};
//用于比较两个点的深度和法线纹理中采样得到的信息 用来判断是否是边缘
//返回值的含义
//1 - 法线和深度值基本相同 处于同一个平面上
//0 - 差异大 不在一个平面上
half CheckSame(half4 depthNormal1, half4 depthNormal2)
{
//分别得到两个点信息的深度和法线
//第一个点的
//得到深度值
float depth1 = DecodeFloatRG(depthNormal1.zw);
//得到法线的xy
float2 normal1 = depthNormal1.xy;
//第二个点的
//得到深度值
float depth2 = DecodeFloatRG(depthNormal2.zw);
//得到法线的xy
float2 normal2 = depthNormal2.xy;
//法线的差异计算
//计算两条法线的xy的差值 并且乘以 自定义的敏感度
float2 normalDiff = abs(normal1 - normal2) * _SensitivityNormal;
//判断两个法线是否在一个平面
//如果差异不大 证明基本上在一个平面上 返回 1;否则返回0
int isSameNormal = (normalDiff.x + normalDiff.y) < 0.1;
//深度的差异计算
float depthDiff = abs(depth1 - depth2) * _SensitivityDepth;
//判断深度是不是很接近 是不是相当于在一个平面上
//如果满足条件 证明深度值差异非常小 基本趋近于在一个平面上 返回1;否则 返回0
int isSameDepth = depthDiff < 0.1 * depth1;
//返回值的含义
//1 - 法线和深度值基本相同 处于同一个平面上
//0 - 差异大 不在一个平面上
return isSameDepth * isSameNormal ? 1 : 0;
}
//顶点着色器
v2f vert(appdata_base appdata_base)
{
v2f v2f;
//转换到裁剪空间
v2f.vertex = UnityObjectToClipPos(appdata_base.vertex);
//uv相关坐标赋值
half2 uv = appdata_base.texcoord;
//中心点
v2f.uv[0] = uv;
//对角线相关像素uv坐标赋值
//左上角
v2f.uv[1] = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance;
//右下角
v2f.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance;
//右上角
v2f.uv[3] = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance;
//左下角
v2f.uv[4] = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance;
return v2f;
}
//片元着色器
fixed4 frag(v2f v2f) : SV_Target
{
// 从深度+法线纹理中采样四个邻近点的深度和法线信息
// 左上角像素的深度和法线信息
half4 TL = tex2D(_CameraDepthNormalsTexture, v2f.uv[1]);
// 右下角像素的深度和法线信息
half4 BR = tex2D(_CameraDepthNormalsTexture, v2f.uv[2]);
// 右上角像素的深度和法线信息
half4 TR = tex2D(_CameraDepthNormalsTexture, v2f.uv[3]);
// 左下角像素的深度和法线信息
half4 BL = tex2D(_CameraDepthNormalsTexture, v2f.uv[4]);
// 初始化边缘插值值,默认设为 1(表示未检测到边缘)
half edgeLerpValue = 1;
// 比较对角线上两个点的深度和法线信息
// 如果两个点深度和法线的差异较大(即不在同一平面),edgeLerpValue 会被置为 0
edgeLerpValue *= CheckSame(TL, BR); // 左上和右下
edgeLerpValue *= CheckSame(TR, BL); // 右上和左下
// 根据检测到的边缘信息对颜色进行插值
// 根据是否是边缘,混合边缘颜色 (_EdgeColor) 和场景原始颜色 (tex2D(_MainTex, v2f.uv[0]))
// 如果 edgeLerpValue 为 1,显示场景颜色;如果为 0,显示边缘颜色
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, v2f.uv[0]), edgeLerpValue);
// 根据是否是边缘,边缘颜色与自定义背景颜色 (_BackgroundColor) 进行插值
// edgeLerpValue 为 1 时显示背景颜色;为 0 时显示边缘颜色
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edgeLerpValue);
// 将计算后的混合原纹理颜色和混合自定义背景颜色插值
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
}
}
Fallback Off// 不使用回退Shader
}
Lesson80_EdgeDetectionWithDepthNormalsTexture.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 自定义的屏幕后处理效果,用于基于深度和法线纹理的边缘检测
public class Lesson80_EdgeDetectionWithDepthNormalsTexture : PostEffectBase
{
// 边缘显示程度,范围 [0, 1]
// 0 表示显示完整的原始场景,1 表示只显示边缘
[Range(0, 1)] public float edgeOnly = 0;
// 边缘的描边颜色
public Color edgeColor = Color.black;
// 背景的自定义颜色
public Color backgroundColor = Color.white;
// 采样偏移量,用于控制边缘粗细
public float sampleDistance = 1;
// 对深度敏感程度,用于判断是否是边缘
public float sensitivityDepth = 1;
// 对法线敏感程度,用于判断是否是边缘
public float sensitivityNormal = 1;
void Start()
{
// 确保摄像机启用深度+法线纹理模式,以便采样深度和法线信息
// 使用按位或操作防止覆盖其他后处理需求
Camera.main.depthTextureMode |= DepthTextureMode.DepthNormals;
}
// 更新材质属性的方法,由基类调用
protected override void UpdateProperty()
{
// 如果材质存在,更新其对应的参数值
if (material != null)
{
// 设置边缘显示程度
material.SetFloat("_EdgeOnly", edgeOnly);
// 设置边缘颜色
material.SetColor("_EdgeColor", edgeColor);
// 设置背景颜色
material.SetColor("_BackgroundColor", backgroundColor);
// 设置采样偏移量(边缘粗细)
material.SetFloat("_SampleDistance", sampleDistance);
// 设置深度敏感度
material.SetFloat("_SensitivityDepth", sensitivityDepth);
// 设置法线敏感度
material.SetFloat("_SensitivityNormal", sensitivityNormal);
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com