61.边缘检测具体实现

61.屏幕后期处理效果-效果实现-边缘检测-具体实现


61.1 知识点

知识回顾

  1. 灰度值公式: L = 0.2126 * R + 0.7152 * G + 0.0722 * B

  2. 边缘检测效果的基本原理:
    获取当前像素以及其上下左右、左上左下、右上右下共9个像素的灰度值。
    使用这9个灰度值与Sobel算子进行卷积计算,得到梯度值:
    G = abs(Gx) + abs(Gy)
    最终颜色 = lerp(原始颜色,描边颜色,梯度值)

  3. 如何得到当前像素周围8个像素的位置:
    使用 float4 纹理变量 _TexelSize 获取当前像素周围8个像素的位置。

准备工作

  • 导入图片资源并设置为Sprite。
  • 新建场景。
  • 在场景中使用导入资源新建Sprite对象,并将其填充满Game窗口进行测试。


实现边缘检测屏幕后期处理效果对应 Shader

主要步骤

  1. 新建一个 Shader,命名为 边缘检测Lesson61_EdgeDetection,并删除无用代码。

  2. 声明属性并进行属性映射:

    • 主纹理 _MainTex
    • 边缘描边颜色 _EdgeColor
    • 注意属性映射时使用内置纹素变量 _MainTex_TexelSize
  3. 配置屏幕后处理效果的标配:

    • ZTest Always(始终通过深度测试)
    • Cull Off(关闭背面剔除)
    • ZWrite Off(关闭深度写入)
  4. 声明结构体,包括顶点和 uv 数组,用于存储 9 个像素点的 uv 坐标。

  5. 顶点着色器:

    • 顶点坐标转换
    • 使用 uv 数组装载 9 个像素的 uv 坐标
  6. 片元着色器:

    • 利用卷积计算获取梯度值(可以声明一个 Sobel 算子计算函数和一个灰度值计算函数)
    • 根据梯度值,在原始颜色和边缘颜色之间进行插值,得到最终颜色
  7. 配置 FallBack Off,不使用任何备用着色器。

新建Shader,取名边缘检测Lesson61_EdgeDetection,删除无用代码,保留骨架

Shader "Unlit/Lesson61_EdgeDetection"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque"
        }

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
    
            ENDCG
        }
    }
}

声明属性,进行属性映射。包括主纹理和 边缘描边用的颜色 _EdgeColor。注意属性映射时,使用内置纹素变量 _MainTex_TexelSize

Properties
{
    // 主纹理
    _MainTex ("Texture", 2D) = "white" {}
    // 边缘线的颜色
    _EdgeColor("EdgeColor", Color) = (0,0,0,0)
}
sampler2D _MainTex; // 主纹理采样器
half4 _MainTex_TexelSize; // 用于存储纹理大小的内置变量
fixed4 _EdgeColor; // 边缘颜色

屏幕后处理效果需要始终通过深度测试,关闭背面剔除和关闭深度写入

ZTest Always // 始终通过深度测试
Cull Off // 关闭背面剔除
ZWrite Off // 关闭深度写入

结构体相关要声明顶点和uv数组,uv数组用于存储9个像素点的uv坐标

struct v2f
{
    // 用于存储9个像素的uv坐标
    half2 uv[9] : TEXCOORD0;
    float4 vertex : SV_POSITION; // 顶点位置
};

顶点函数将顶点从物体空间转换为裁剪空间,并且通过纹素计算9个像素的uv坐标

// 顶点着色器
v2f vert(appdata_base appdata_base)
{
    v2f v2f;
    v2f.vertex = UnityObjectToClipPos(appdata_base.vertex); // 将顶点从物体空间转换为裁剪空间
    
    // 当前顶点的纹理坐标
    half2 uv = appdata_base.texcoord;
    
    // 计算9个像素的uv坐标
    v2f.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
    v2f.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
    v2f.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
    v2f.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
    v2f.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
    v2f.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
    v2f.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
    v2f.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
    v2f.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);

    return v2f;
}

基于灰度公式,实现计算颜色的灰度值函数

// 计算颜色的灰度值
fixed calcGrayValue(fixed4 color)
{
    return 0.2126 * color.r + 0.7152 * color.g + 0.722 * color.b;
}

声明Soel算子函数,声明Sobel算子对应的两个卷积核,遍历相乘后累加返回梯度值和

// Sobel算子相关的卷积计算
half Sobel(v2f v2f)
{
    // Sobel算子对应的两个卷积核
    half Gx[9] = {
        -1, -2, -1,
        0, 0, 0,
        1, 2, 1
    };
    half Gy[9] = {
        -1, 0, 1,
        -2, 0, 2,
        -1, 0, 1
    };
    
    half nowGrayValue; // 灰度值
    half edgeX = 0; // 水平方向的梯度值
    half edgeY = 0; // 垂直方向的梯度值
    
    for (int i = 0; i < 9; i++)
    {
        // 使用对应uv采样颜色 并计算灰度值
        nowGrayValue = calcGrayValue(tex2D(_MainTex, v2f.uv[i]));
        // 得到灰度值和对应卷积核相乘后加到梯度值中
        edgeX += nowGrayValue * Gx[i];
        edgeY += nowGrayValue * Gy[i];
    }
    
    // 计算该像素的最终梯度值
    return abs(edgeX) + abs(edgeY);
}

片元函数中使用Sobel算子计算当前像素的梯度值,根据计算得到的梯度值,在原始颜色和边缘颜色之间进行插值

// 片段着色器
fixed4 frag(v2f v2f) : SV_Target
{
    // 使用Sobel算子计算当前像素的梯度值
    half edge = Sobel(v2f);
    
    // 根据计算得到的梯度值,在原始颜色和边缘颜色之间进行插值
    fixed4 color = lerp(tex2D(_MainTex, v2f.uv[4]), _EdgeColor, edge);
    
    return color;
}

不使用备用着色器

Fallback Off // 不使用任何备用着色器

实现边缘检测屏幕后期处理效果对应 C# 代码

主要步骤

  1. 创建一个 C# 脚本,命名为 EdgeDetection(边缘检测)。

  2. 继承屏幕后处理基类 PostEffectBase

  3. 声明边缘颜色变量,用于控制效果变化。

  4. 重写 UpdateProperty 方法,设置材质球的属性。

实现C#脚本

public class Lesson61_EdgeDetection : PostEffectBase
{
    public Color EdgeColor;

    protected override void UpdateProperty()
    {
        if (material != null)
        {
            material.SetColor("_EdgeColor", EdgeColor);
        }
    }
}

挂载C#脚本到摄像机伤害,拖入shader,可以看到边缘线的效果



61.2 知识点代码

Lesson61_屏幕后期处理效果_效果实现_边缘检测_具体实现.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Lesson61_屏幕后期处理效果_效果实现_边缘检测_具体实现 : MonoBehaviour
{
    void Start()
    {
        #region 知识回顾

        //1.灰度值 L = 0.2126*R + 0.7152*G + 0.0722*B
        //2.边缘检测效果的基本原理
        //  得到 当前像素以及其 上下左右、左上左下、右上右下共9个像素的灰度值
        //  用这9个灰度值和 Sobel算子 进行卷积计算得到梯度值 G = abs(Gx) + abs(Gy)
        //  最终颜色 = lerp(原始颜色,描边颜色,梯度值)
        //3.如何得到当前像素周围8个像素位置
        //  利用 float4 纹理名_TexelSize 纹素 信息得到当前像素周围8个像素位置

        #endregion

        #region 准备工作

        //1.导入图片资源 设置为Sprite
        //2.新建场景
        //3.在场景中使用导入资源新建Sprite对象 将其填充满Game窗口 用于测试

        #endregion

        #region 知识点一 实现边缘检测屏幕后期处理效果对应 Shader

        //1.新建Shader,取名边缘检测Lesson61_EdgeDetection,删除无用代码
        
        //2.声明属性,进行属性映射
        //  主纹理 _MainTex
        //  边缘描边用的颜色 _EdgeColor
        //  注意属性映射时 使用内置纹素变量 _MainTex_TexelSize
        
        //3.屏幕后处理效果标配
        //  ZTest Always
        //  Cull Off
        //  ZWrite Off
        
        //4.结构体相关
        //  顶点
        //  uv数组,用于存储9个像素点的uv坐标
        
        //5.顶点着色器
        //  顶点坐标转换
        //  用uv数组装载9个像素uv坐标
        
        //6.片元着色器
        //  利用卷积获取梯度值(可以声明一个Sobel算子计算函数和一个灰度值计算函数)
        //  利用梯度值在原始颜色和边缘颜色之间进行插值得到最终颜色
        
        //7.FallBack Off

        #endregion

        #region 知识点二 实现边缘检测屏幕后期处理效果对应 C#代码

        //1.创建C#脚本,名为EdgeDetection(边缘检测)
        //2.继承屏幕后处理基类PostEffectBase
        //3.声明边缘颜色变量,用于控制效果变化
        //4.重写UpdateProperty方法,设置材质球属性

        #endregion
    }
}

Lesson61_EdgeDetection.shader

Shader "Unlit/Lesson61_EdgeDetection"
{
    Properties
    {
        // 主纹理
        _MainTex ("Texture", 2D) = "white" {}
        // 边缘线的颜色
        _EdgeColor("EdgeColor", Color) = (0,0,0,0)
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque" // 渲染类型为不透明
        }

        Pass
        {
            ZTest Always // 始终通过深度测试
            Cull Off // 关闭背面剔除
            ZWrite Off // 关闭深度写入

            CGPROGRAM
            #pragma vertex vert  
            #pragma fragment frag  

            #include "UnityCG.cginc"  

            sampler2D _MainTex; // 主纹理采样器
            half4 _MainTex_TexelSize; // 用于存储纹理大小的内置变量
            fixed4 _EdgeColor; // 边缘颜色

            struct v2f
            {
                // 用于存储9个像素的uv坐标
                half2 uv[9] : TEXCOORD0;
                float4 vertex : SV_POSITION; // 顶点位置
            };

            // 顶点着色器
            v2f vert(appdata_base appdata_base)
            {
                v2f v2f;
                v2f.vertex = UnityObjectToClipPos(appdata_base.vertex); // 将顶点从物体空间转换为裁剪空间
                
                // 当前顶点的纹理坐标
                half2 uv = appdata_base.texcoord;
                
                // 计算9个像素的uv坐标
                v2f.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
                v2f.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
                v2f.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
                v2f.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
                v2f.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
                v2f.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
                v2f.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
                v2f.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
                v2f.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);

                return v2f;
            }

            // 计算颜色的灰度值
            fixed calcGrayValue(fixed4 color)
            {
                return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
            }

            // Sobel算子相关的卷积计算
            half Sobel(v2f v2f)
            {
                // Sobel算子对应的两个卷积核
                half Gx[9] = {
                    -1, -2, -1,
                    0, 0, 0,
                    1, 2, 1
                };
                half Gy[9] = {
                    -1, 0, 1,
                    -2, 0, 2,
                    -1, 0, 1
                };
                
                half nowGrayValue; // 灰度值
                half edgeX = 0; // 水平方向的梯度值
                half edgeY = 0; // 垂直方向的梯度值
                
                for (int i = 0; i < 9; i++)
                {
                    // 使用对应uv采样颜色 并计算灰度值
                    nowGrayValue = calcGrayValue(tex2D(_MainTex, v2f.uv[i]));
                    // 得到灰度值和对应卷积核相乘后加到梯度值中
                    edgeX += nowGrayValue * Gx[i];
                    edgeY += nowGrayValue * Gy[i];
                }
                
                // 计算该像素的最终梯度值
                return abs(edgeX) + abs(edgeY);
            }

            // 片段着色器
            fixed4 frag(v2f v2f) : SV_Target
            {
                // 使用Sobel算子计算当前像素的梯度值
                half edge = Sobel(v2f);
                
                // 根据计算得到的梯度值,在原始颜色和边缘颜色之间进行插值
                fixed4 color = lerp(tex2D(_MainTex, v2f.uv[4]), _EdgeColor, edge);
                
                return color;
            }
            
            ENDCG
        }
    }

    Fallback Off // 不使用任何备用着色器
}

Lesson61_EdgeDetection.cs

using UnityEngine;


public class Lesson61_EdgeDetection : PostEffectBase
{
    public Color EdgeColor;

    protected override void UpdateProperty()
    {
        if (material != null)
        {
            material.SetColor("_EdgeColor", EdgeColor);
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏