Shader边缘检测的几种实现(卷积、深度、法线)

阅前提示

记录了实现描边效果的几种方法。
适合人群:All
阅读方式:工具文章
参考资料:
https://blog.csdn.net/puppet_master/article/details/83759180
https://www.bilibili.com/video/BV1ia4y177GJ


边缘检测思路

如何确认一张图片中哪些像素代表是边? 这是实现描边效果的关键。
在这里插入图片描述
这是一张2D图片,那如何来定义什么是边呢?
我们可以发现:边缘可以被突出出来是因为它与四周存在明显的差异
从这点,我们找到了关于区分边缘的关键字:差异
接下来作者将会围绕差异来介绍几种边缘检测的方法。


卷积

在图像处理中,卷积操作指使用一个卷积核(Kernel) 对一张图像中每个像素进行一系列操作,这一系列操作往往是对一个四方形的网格结构区域进行不同权重的计算,以得出该像素的卷积,而这个卷积就是评判该像素的重要数据。

卷积核的不同可以达到不同的图像处理效果,例如:
1 / 9 ∗ 1 1 1 1 1 1 1 1 1 1/9*\begin{matrix} 1 & 1 & 1 \\ 1 & 1 & 1 \\ 1 & 1 & 1 \\ \end{matrix} 1/9111111111
模糊:这是对像素点四周3*3的范围像素进行平均取值,来定义该像素点的像素值

G x = − 1 0 − 1 − 2 0 2 − 1 0 1 G y = − 1 − 2 − 1 0 0 0 1 2 1 Gx = \begin{matrix} -1 & 0 & -1 \\ -2 & 0 & 2 \\ -1 & 0 & 1 \\ \end{matrix} Gy = \begin{matrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ 1 & 2 & 1 \\ \end{matrix} Gx=121000121Gy=101202101
边缘检测:这是对像素点四周3*3的范围的X轴Y轴进行采样,以算得该像素的梯度值G(gradient),该值越大说明越可能是边缘。使用的算子不同也会对结果带来影响,以上使用的是Sobel算子。

G = G x 2 + G y 2 出 于 性 能 考 虑 也 可 以 使 用 绝 对 值 来 替 代 开 根 号 G = ∣ G x ∣ + ∣ G y ∣ G = \sqrt{Gx^2 + Gy^2}\\ 出于性能考虑也可以使用绝对值来替代开根号\\ G = |Gx| + |Gy| G=Gx2+Gy2 使G=Gx+Gy

在这里插入图片描述
上图为在Sobel算子下,图像处理效果。
基本的实现代码如下

// 该方法主要是获取3*3的uv坐标以供后续采样需要
void VES_Get_Box9_UV(float2 uv,half4 unit,out float2 boxuv[9])
{
    const half2 BOX_9[9] =
    {
        half2(-1,-1),half2(0,-1),half2(1,-1),
        half2(-1,0), half2(0,0), half2(1,0),
        half2(-1,1), half2(0,1), half2(1,1)
    };

    for(int it = 0;it < 9;it++)
    {
        boxuv[it] = uv + BOX_9[it]*unit.xy;
    }
}

//Sobel效果主体
half VES_Sobel(float2 boxuv[9],sampler2D _Tex)
{
	//两个算子
    const half VES_SOBEL_Gx[9] = 
    {
        -1,-2,-1,
        0, 0, 0,
        1, 2, 1
    };

    const half VES_SOBEL_Gy[9] = 
    {
        -1, 0, 1,
        -2, 0, 2,
        -1, 0, 1
    };

    half texColor;
    half eX = 0;
    half eY = 0;
    //采样求梯度值
    for(int n = 0;n<9;n++)
    {
    	// 这里是针对亮度值进行采样
        texColor = Luminance(tex2D(_Tex,boxuv[n]));
        eX += texColor * VES_SOBEL_Gx[n];
        eY += texColor * VES_SOBEL_Gy[n];
    }
    return 1 - (abs(eX) + abs(eY));
}


介绍完了运用卷积实现边缘检测的图像处理方式后,接下来我们可以拓宽思路,既然边缘检测的关键在于差异,那其实可以产生差异的地方不仅仅只是像素上的(当然这需要是3D物体才会有其他东西)。那么接下来介绍的两种方式都是只可以针对3D物体的边缘检测可以使用的。


深度

如何区分什么为3D物体的边,那很明显,3D物体的每个坐标都会有一个特殊的值Z,也就是深度
深度可以帮助我们区分哪些物体在哪些物体的前方,哪些物体是被遮盖住的。
如果一个位置的深度值和它四周的深度值产生了差异,那说明这个位置可能就是我们想要找的边缘。

在这里插入图片描述
为此,我找了个3D模特,先看一下它在Sobel下的效果如何。
在这里插入图片描述
还是非常不错的,能很准确的区分出边缘。
接下来是根据深度的效果。
在这里插入图片描述
在这里插入图片描述
看起来似乎不尽人意,但实际上这确实是该模型深度值能体现出的效果,以为模型的眼睛鼻子仅仅是一张贴图,不会在深度上有很夸张的变化。

放码如下:

//顶点作色
//这里使用的是Unity相机的depthTextureMode
//即Camera.depthTextureMode = DepthTextureMode.DepthNormals;
//这样可以使用该贴图直接获取相机成像的图片深度法线图。
  sampler2D _CameraDepthNormalsTexture; 
//图片单位尺寸
  half4 _CameraDepthNormalsTexture_TexelSize;
  v2f vert (appdata v)
  {
      v2f o;
      o.vertex = UnityObjectToClipPos(v.vertex);
      //依然是需要获取3*3的区域
      VES_Get_Box9_UV(v.uv,_CameraDepthNormalsTexture_TexelSize,o.uv);
      return o;
  }

//获取深度法线贴图中的深度值与法线
 void ComputeDethpNormal(v2f i,int index,out float depth,out fixed3 normal)
 {
     fixed4 col = tex2D(_CameraDepthNormalsTexture, i.uv[index]);
     DecodeDepthNormal(col,depth,normal);
 }

//片元作色
fixed4 frag (v2f i) : SV_Target
  {
      float depth = 0;
      fixed3 normal;

      fixed4 relCol = tex2D(_MainTex,i.uv[4]); //4为该点真确的uv坐标
      float relDepth;
      fixed3 relNormal;
      ComputeDethpNormal(i,4,relDepth,relNormal);

      for(int n = 0;n<9;n++)
      {
          if(n !=4)
          {
              float _depth;
              float3 _normal; 
              ComputeDethpNormal(i,n,_depth,_normal);
              depth += _depth;
          }
      }
      //深度差异在于采样点深度相差值
      float depthDiff = abs(relDepth - depth/8); 
      //当差异值大于我们预设的阈值时,代表该点为边缘 
      if(depthDiff > _DepthThreshold * 0.01)
      {
          return _EdgeColor; //边缘
      }
      else
          return _BackgroundColor;//非边缘

  }


法线

其实在上文中我们不仅仅可以使用深度,也可以使用法线,法线其实就是判断是否为平行,如果采样出来的法线之间相差较大,那也可以判断为边缘。
但这里就不再过多介绍这种方法那,关于法线,还可以有另一种思路:法线与视角间的夹角
被认定为边缘的部分一般视线与其法线的夹角更接近于垂直
基于这种判断,实现效果如下。
在这里插入图片描述
放码~

     v2f vert (appdata v)
     {
         v2f o;
         o.vertex = UnityObjectToClipPos(v.vertex);
         o.uv = v.uv;
         o.viewDir = normalize(WorldSpaceViewDir(v.vertex)); //获取视角
         o.normal = normalize(UnityObjectToWorldNormal(v.normal)); //法线
         return o;
     }

     fixed4 frag (v2f i) : SV_Target
     {
         fixed4 relCol = tex2D(_MainTex, i.uv);
         //  法线于视角的点乘,越接近于零越垂直
         float NdotV = dot(i.normal,i.viewDir); 
         //小于某个阈值代表为边界
         if(NdotV < _NdotVThreshold)
             return _EdgeColor;
         else
             return _BackgroundColor;
     }

.
.
.
.
.


嗨,我是作者Vin129,逐儿时之梦正在游戏制作的技术海洋中漂泊。知道的越多,不知道的也越多。希望我的文章对你有所帮助:)


  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值