RedZero9 发表于 2022-11-16 09:20

《Unity Shader入门精要》笔记(二十六)

本文为《Unity Shader入门精要》第十三章《使用深度和法线纹理》的第四节内容《再谈边缘检测》。
本文相关代码,详见:

原书代码,详见原作者github:

<hr/>1. 概念原理

12章时使用Sobel算子对屏幕图像进行边缘检测,这种直接利用颜色信息进行边缘检测的方法会受纹理、阴影等因素的影响,导致最终的描边并不精确。
本章将利用深度和法线纹理,通过Roberts算子进行边缘检测,得到的结果将更加可靠。

1.1 Roberts算子




Roberts算子的本质:计算左上角和右下角的差值,乘以右上角和左下角的差值,作为评估的依据。
我们将按这样的方式,取对角方向的深度或法线值,比较它们之间的差值,如果超过某个阈值,则认为他们之间存在一条边。

2. 案例:使用Roberts算子+深度法线纹理实现边缘检测

2.1 效果预览

本案例将实现如下效果:



2.2 准备工作

完成如下准备工作:

[*]新建名为Scene_13_4的场景,并去掉天空盒;
[*]搭建类似上一节《全局雾效》的场景,详细可参考文章开头的工程;
[*]将上一节中用到的TranslatingC#脚本拖拽给相机;
[*]新建名为EdgeDetectNormalsAndDepth的C#脚本,并拖拽给相机;
[*]新建名为Chapter13-EdgeDetectNormalAndDepth的Unity Shader;
[*]保存场景。

2.2 编写C#脚本:EdgeDetectNormalsAndDepth

编写如下代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EdgeDetectNormalsAndDepth : PostEffectsBase
{
    // 边缘检测Shader
    public Shader edgeDetectShader;
    // 边缘检测材质
    private Material edgeDetectMaterial = null;
    public Material material
    {
      get
      {
            edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
            return edgeDetectMaterial;
      }
    }

   
    public float edgeOnly = 0.0f;
    // 边缘颜色
    public Color edgeColor = Color.black;
    // 背景颜色
    public Color backgroundColor = Color.white;
    // 控制深度法线纹理的采样距离,值越大,描边越宽
    public float sampleDistance = 1.0f;
    // 影响当邻域的深度值相差多少时,认定为存在边界
    public float sensitivityDepth = 1.0f;
    // 影响当邻域的法线值相差多少时,认定为存在边界
    public float sensitivityNormals = 1.0f;

    void OnEnable()
    {
      // 开启深度+法线纹理
      GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    // 添加ImageEffectOpaque属性,让OnRenderImage方法在不透明的Pass执行完后立刻被调用
    // (在透明Pass前,从而只对不透明物体进行描边)
   
    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
      if (material == null)
      {
            Graphics.Blit(src, dest);
            return;
      }

      // 传递参数到Shader
      material.SetFloat("_EdgeOnly", edgeOnly);
      material.SetColor("_EdgeColor", edgeColor);
      material.SetColor("_BackgroundColor", backgroundColor);
      material.SetFloat("_SampleDistance", sampleDistance);
      material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));

      Graphics.Blit(src, dest, material);
    }
}

2.3 编写Shader:Chapter13-EdgeDetectNormalAndDepth

编写如下代码:
Shader "Unity Shades Book/Chapter 13/Edge Detect Normal And Depth"
{
    Properties
    {
      _MainTex ("Base (RGB)", 2D) = "white" {}
      _EdgeOnly ("Edge Only", Float) = 1.0
      _EdgeColor ("Edge Color", Color) = (0.0, 0.0, 0.0, 1.0)
      _BackgroundColor ("Background Color", Color) = (1.0, 1.0, 1.0, 1.0)
      _SampleDistance ("Sample Distance", Float) = 1.0
      _Sensitivity ("Sensitivity", Vector) = (1.0, 1.0, 1.0, 1.0)
    }

    SubShader
    {
      CGINCLUDE

      #include "UnityCG.cginc"

      // 主纹理
      sampler2D _MainTex;
      // 主纹理的纹素
      half4 _MainTex_TexelSize;
      // 只显示边缘
      fixed _EdgeOnly;
      // 边缘颜色
      fixed4 _EdgeColor;
      // 背景颜色
      fixed4 _BackgroundColor;
      // 采样距离
      float _SampleDistance;
      // 控制判定为边界的密度(只用到x、y分量)
      half4 _Sensitivity;
      sampler2D _CameraDepthNormalsTexture;

      struct v2f
      {
            float4 pos : SV_POSITION;
            // 存储当前片元的UV值以及邻域四个片元的UV值
            // (右上、左下、左上、右下)
            half2 uv : TEXCOORD0;
      };

      v2f vert(appdata_img v)
      {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);

            half2 uv = v.texcoord;
            o.uv = uv;

            #if UNITY_UV_STARTS_AT_TOP
            if (_MainTex_TexelSize.y < 0)
                uv.y = 1 - uv.y;
            #endif

            // 存储邻域UV值,用于片元着色器使用Roberts算子时的采样坐标
            o.uv = uv + _MainTex_TexelSize.xy * half2(1, 1) * _SampleDistance;
            o.uv = uv + _MainTex_TexelSize.xy * half2(-1, -1) * _SampleDistance;
            o.uv = uv + _MainTex_TexelSize.xy * half2(-1, 1) * _SampleDistance;
            o.uv = uv + _MainTex_TexelSize.xy * half2(1, -1) * _SampleDistance;

            return o;
      }

      half CheckSame(half4 center, half4 sample)
      {
            // 注意:
            // 这里因为只需要比较法线值,所以不需要精确地求得真正的法线值
            // (没有像上一节一样将采样的法线值转为视角空间下的线性法线值)

            // 得到法线1
            half2 centerNormal = center.xy;
            // 得到深度值1(本质是z w分量相加得到一个数值)
            /**
            inline float DecodeFloatRG( float2 enc )
            {
                float2 kDecodeDot = float2(1.0, 1/255.0);
                return dot( enc, kDecodeDot );
            }
            */
            // 等同于 x / 255 + y
            float centerDepth = DecodeFloatRG(center.zw);

            // 得到法线2
            half2 sampleNormal = sample.xy;
            // 得到深度值2
            float sampleDepth = DecodeFloatRG(sample.zw);

            // 计算法线的偏移程度,用_Sensitivity.x来控制判定程度
            half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
            // 取法线偏差的x、y相加(经验模型)来判定两条法线是否接近相同的方向
            int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;

            // 计算深度的相差程度,用_Sensitivity.y来控制判定程度
            float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
            // 用偏差是否达到其中一个深度的十分之一来判定深度值是否相近
            int isSameDepth = diffDepth < 0.1 * centerDepth;

            // 只有深度和法线都差不多接近,才返回1;
            // 否则返回0
            return isSameNormal * isSameDepth ? 1.0 : 0.0;
      }

      fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target
      {
            // 对深度法线纹理,当前像素的邻域进行采样
            half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv);
            half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv);
            half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv);
            half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv);

            // 代入Roberts算子的公式:
            // 左上、右下差值 * 左下、右上差值
            // edge的最终值只会是0或1,只要任意一个不是相近,则判定它是边界
            half edge = 1.0;
            edge *= CheckSame(sample1, sample2);
            edge *= CheckSame(sample3, sample4);

            // 边缘颜色、当前像素颜色的插值
            fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv), edge);
            // 边缘颜色、背景色的插值
            fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);

            // 根据_EdgeOnly参数,来控制是否结合像素颜色的程度
            return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
      }

      ENDCG

      Pass
      {
            ZTest Always
            Cull Off
            ZWrite Off

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment fragRobertsCrossDepthAndNormal

            ENDCG
      }
    }

    Fallback Off
}

2.4 配置参数

配置如下参数,即可得到最终的案例效果:



3. 扩展

如果想实现只针对特定物体进行表面的效果,Unity提供的Graphics.DrawMesh或Graphics.DrawMeshNow函数,把需要描边的物体再次渲染一次(在所有不透明物体渲染完毕之后),再使用本节提到的边缘检测算法计算深度或法线纹理中每个像素的梯度值,判断它们是否小于阈值。若是,则在Shader中使用Clip()函数将该像素剔除掉,从而显示出原来的颜色。

4. 本章扩展

深度和法线纹理在屏幕特效的实现中扮演很重要的角色,很多效果都可以基于深度和法线纹理去实现,原作者给出了Unity在2011年的SIGGRAPH上做的一个关于使用深度纹理实现各种特效的演讲,感兴趣的小伙伴可前往阅读:
https://blog.unity.com/community/special-effects-with-depth-talk-at-siggraph。

以上是本次笔记的所有内容,下一篇笔记,我们将开启新的章节,也是个人觉得最有趣的一章——《非真实感渲染》。

<hr/>
写在最后

本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder)
页: [1]
查看完整版本: 《Unity Shader入门精要》笔记(二十六)