US4L2——模型描边效果

知识回顾 —— 边缘检测

边缘检测效果,是一种用于突出图像中的边缘,使物体的轮廓更加明显的图像处理技术。
我们目前学习的实现方式有两种,都是基于屏幕后期处理效果的:

  1. 基于卷积运算的:

    用像素周围的9个像素的灰度值和Sobel算子进行卷积计算得到梯度值决定是否在边缘上

  2. 基于深度+法线纹理的:

    基于Roberts交叉算子,通过比较对角线上的的像素的深度和法线值,判断是否在边缘上

这两种方式会对屏幕图像中每个像素产生影响,主要是针对全局性效果的,一个用于2D图像,一个用于3D场景

模型描边效果

在 Unity 中,模型描边效果也可以称为边缘描边轮廓描边轮廓线效果
它实现出来的效果类似边缘检测,主要是为了让单个 3D 模型产生描边效果,使模型的轮廓更加突出。
这种效果一般用在卡通渲染、手绘风格、漫画风格的游戏中,还可以用来制作对象选中效果。

image

模型描边效果的基本原理

一句话描述模型描边效果的基本原理:
两个 Pass​ 渲染对象,一个 Pass​ 用于渲染沿法线方向放大的模型,一个 Pass​ 用于正常渲染正常模型。
相当于先用纯色渲染一次放大后的模型,再用模型本来的颜色覆盖重合部分

关键点:

  1. 如何放大模型

    在第一个 Pass​ 中,我们可以在顶点着色器函数中,将顶点沿着法线方向进行偏移(相当于膨胀了模型),
    偏移的距离可以是一个自定义参数,它可以用来控制边缘线的粗细。
    在片元着色器函数中直接返回一个自定义颜色即可,它决定了边缘线的颜色。

    注意:此方案对于棱角分明的几何体(例如立方体)效果较差,更好最简单的方法是采用外壳模型(也就是额外使用一个更大的模型)的方案

    image

  2. 如何覆盖重合部分

    在第二个 Pass​ 中,我们只需要正常按需求(是否受光照影响等)渲染模型即可,
    但是正常情况下,放大的部分会直接挡住正常大小的模型内容,这是因为第二个 Pass​ 的渲染的重合部分无法通过深度写入,直接被抛弃了

    image

    要解决这个问题很简单,直接让第一个 Pass 关闭深度写入即可,这样重合部分,就会显示为第二个 Pass 绘制的结果

总结:使用两个 Pass​ 渲染对象,一个 Pass​ 用于渲染沿法线方向放大的模型,一个 Pass​ 用于正常渲染正常模型。
相当于先用纯色渲染一次放大后的模型,再用模型本来的颜色覆盖重合部分。
为了让重合部分能够正常显示为第二个 Pass​ 的渲染内容,需要关闭第一个 Pass​ 的深度写入!

解决关闭深度写入带来的问题

虽然关闭第一个 Pass​ 的深度写入可以帮助我们实现出想要的效果,但是它会带来一个问题,就是物体后方的内容会覆盖掉边缘线

imageimage

解决这个问题的方案是:设置该模型描边效果 Shader 的渲染队列为 Transparent​,让它晚于几何体进行渲染

模型描边效果 具体实现

  1. 新建 Shader 取名 OutLine

    删除无用代码

  2. 属性声明

    • 边缘线颜色 _OutLineColor
    • 边缘线粗细 _OutLineWidth
    1
    2
    3
    4
    5
    6
    Properties
    {
    _MainTex ("Texture", 2D) = "white"{}
    _OutLineColor("OutLineColor", Color) = (1,1,1,1)
    _OutLineWidth("OutLineWidth", Float) = 0.01
    }
  3. 修改渲染队列为 Transparent

    1
    2
    3
    4
    5
    SubShader
    {
    Tags { "RenderType"="Opaque" "Queue"="Transparent" }
    // ...
    }
  4. 复制一个 Pass

  5. 编写第一个 Pass

    1. 关闭深度写入

      1
      2
      3
      4
      5
      6
      7
      8
      Pass
      {
      ZWrite Off // 关闭深度写入,目的是让第二个Pass能够覆盖重合的地方

      CGPROGRAM
      //...
      ENDCG
      }
    2. 属性映射

    3. 结构体相关

      不需要纹理坐标,因为描边颜色是固定的,不需要采样

      1
      2
      3
      4
      5
      6
      7
      8
      struct v2f
      {
      float4 vertex : SV_POSITION;
      };

      sampler2D _MainTex;
      fixed4 _OutLineColor; // 边缘线颜色
      fixed _OutLineWidth; // 边缘线粗细
    4. 顶点着色器

      顶点朝法线方向进行偏移后再转换到裁剪空间

      1
      2
      3
      4
      5
      6
      7
      8
      v2f vert (appdata_base v)
      {
      v2f o;
      // 偏移顶点位置,朝法线方向偏移
      float4 newVertex = v.vertex + float4(normalize(v.normal) * _OutLineWidth, 0);
      o.vertex = UnityObjectToClipPos(newVertex); // 顶点转换到裁剪空间
      return o;
      }
    5. 片元着色器

      直接返回边缘线颜色

      1
      2
      3
      4
      fixed4 frag (v2f i) : SV_Target
      {
      return _OutLineColor;
      }
  6. 编写第二个 Pass

    编写传统的纹理采样 Pass​ 进行测试即可(如果有光照相关的需求 自定添加即可)

    这里直接复用标准漫反射的两个Pass,代码详见:US3S7L1——标准漫反射Shader

    1
    2
    3
    4
    5
    // 基础渲染通道 Base Pass
    UsePass "TeachShader/BumpedDiffuse/BasePass"

    // 附加渲染通道 Additional Pass
    UsePass "TeachShader/BumpedDiffuse/AdditionalPass"
  7. FallBack "Diffuse"

完整 Shader 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Shader "TeachShader/OutLine"
{
Properties
{
_Color("MainColor", Color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_BumpScale("BumpScale", Range(0, 1)) = 1
_OutLineColor("OutLineColor", Color) = (1,1,1,1)
_OutLineWidth("OutLineWidth", Float) = 0.01
}

SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Transparent" }

// 描边渲染通道 OutLine Pass
Pass
{
ZWrite Off // 关闭深度写入,目的是让第二个Pass能够覆盖重合的地方

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
fixed4 _OutLineColor; // 边缘线颜色
fixed _OutLineWidth; // 边缘线粗细

v2f vert (appdata_base v)
{
v2f o;
// 偏移顶点位置,朝法线方向偏移
float4 newVertex = v.vertex + float4(normalize(v.normal) * _OutLineWidth, 0);
o.vertex = UnityObjectToClipPos(newVertex); // 顶点转换到裁剪空间
return o;
}

fixed4 frag (v2f i) : SV_Target
{
return _OutLineColor;
}
ENDCG
}

// 基础渲染通道 Base Pass
UsePass "TeachShader/BumpedDiffuse/BasePass"

// 附加渲染通道 Additional Pass
UsePass "TeachShader/BumpedDiffuse/AdditionalPass"
}

Fallback "Diffuse"
}

显示效果(边缘线颜色为白色,边缘线粗细为0.01):

image

可见,模型边缘显示出了白色的描边