Unity Shader学习笔记(26)边缘检测(深度和法线纹理)

发表于2018-01-02
评论0 4.7k浏览
边缘检测的原理是利用一些边缘检测算子对图像进行卷积操作,下面就给大家介绍下边缘检测,一起来看看吧。

边缘检测

使用深度和法线纹理实现,是不受图像纹理和光照影响的,仅保存了当前渲染物体的模型信息。这里使用Roberts算子实现,采样的像素法线变化大的(差值大于0.1)或深度变化大的作为边。 

using UnityEngine;
/// <summary>
/// 边缘检测(法线和深度纹理上进行,之前的是对颜色处理)
/// </summary>
public class EdgeDetectNormalsAndDepth : PostEffectsBase
{
    [Range(0.0f, 1.0f)]
    public float edgesOnly = 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]     // 不透明物体渲染完后执行(不影响透明物体),因为默认是在不透明和透明物体都渲染完才调用的。
    void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (TargetMaterial != null)
        {
            ... // 把上面变量都传入Shader
        }
        Graphics.Blit(src, dest, TargetMaterial);
    }
}

Shdaer:

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)
    _SampleDistance ("Sample Distance", Float) = 1.0
    _Sensitivity ("Sensitivity", Vector) = (1, 1, 1, 1)     // x作为法线的灵敏度,y作为深度的灵敏度,zw没用
}
SubShader {
    CGINCLUDE
    ...
    sampler2D _CameraDepthNormalsTexture;
    struct v2f {
        float4 pos : SV_POSITION;
        half2 uv[5]: TEXCOORD0;     // 第一组存屏幕颜色采样纹理,剩下四个Roberts算子采样的纹理坐标
    };
    v2f vert(appdata_img v) {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        half2 uv = v.texcoord;
        o.uv[0] = uv;
        #if UNITY_UV_STARTS_AT_TOP
        if (_MainTex_TexelSize.y < 0)
            uv.y = 1 - uv.y;
        #endif
        // Roberts算子
        o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
        o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
        o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
        o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;
        return o;
    }
    fixed4 fragRobertsCrossDepthAndNormal(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]);
        half edge = 1.0;
        // 两个纹理差值 0为有边
        edge *= CheckSame(sample1, sample2);
        edge *= CheckSame(sample3, sample4);
        fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
        fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
        return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
    }
    // 采样判断两点间是否存在一条边,存在返回0
    half CheckSame(half4 center, half4 sample) {
        half2 centerNormal = center.xy;
        float centerDepth = DecodeFloatRG(center.zw);
        half2 sampleNormal = sample.xy;
        float sampleDepth = DecodeFloatRG(sample.zw);
        // 法线差值,差值大于0.1判断为变换明显,作为边。
        half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;
        int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;
        // 深度差值,差值大于0.1判断为变换明显,作为边。
        float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;
        int isSameDepth = diffDepth < 0.1;
        // 法线 或 深度 其中一个相差大(为0)就是边
        return isSameNormal * isSameDepth ? 1.0 : 0.0;
    }
    ENDCG
    Pass { 
        ZTest Always Cull Off ZWrite Off
        CGPROGRAM      
        #pragma vertex vert  
        #pragma fragment fragRobertsCrossDepthAndNormal
        ENDCG  
    }
} 

要单独对某个物体描边,可以使用Graphics.DrawMesh把目标物体再渲染一遍(即渲染了两层),在通过边缘检测算法把小于阈值的剔除掉即可。

Unity Shader学习笔记系列教程:

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引