Zack.Zhang Game Developer

虚幻4卡通描边(Toon Outlines)教程(翻译)

2021-11-17
zack.zhang

原文:《Unreal Engine 4 Toon Outlines Tutorial》

当人们说“卡通描边”时,他们指的是在物体周围绘制线条的那些技术。像卡通着色一样,描边可以帮助你的游戏看起来更风格化。它们可以给人一种物体是手绘出来的印象。你可以在《大神》(Okami)、《无主之地》和《龙珠战士Z》(Dragon Ball FighterZ)等游戏中看到这种情况。

在本教程中,你将学习如何:

  • 使用反转网格法来创建描边

  • 使用后处理和卷积创建描边

  • 创建和使用材质函数

  • 采样相邻像素

注意:本教程假设你已经知道使用虚幻引擎的基础知识。如果你是虚幻引擎的新手,你应该首先回顾一下我们前面的10部分虚幻引擎初学者教程系列。

注意:本教程是虚幻引擎着色器教程系列的四个部分中的一部分:
第一部分:卡通渲染(Cel-Shading)
第二部分:卡通描边(Toon Outlines)(你在这儿)
第三部分:使用HLSL定制着色器
第四部分:油画滤镜

开始

下载工程(提取码:2oob),解压缩,进入ToonOutlineStarter文件夹并打开工程ToonOutline.uproject。你将看到如下场景:

unreal-engine-toon-outline-01

首先,你将使用反转网格法来创建描边。

反转网格描边

这种方法背后的思想是复制你的目标网格,然后,给复制的网格赋一个纯色(通常是黑色),并扩大它,使它比原来的网格略大。这样你就会得到一个描边。

unreal-engine-toon-outline-02

如果你直接使用复制的网格,它会把原网格完全挡住的。

unreal-engine-toon-outline-01

为了解决这个问题,你可以反转复制网格的法线。启用了背面剔除,你将看到内部的面而不是外部的面。

unreal-engine-toon-outline-02

这会让原网格透过复制网格显示出来,因为复制的网格比原网格大,你将得到一个描边。

unreal-engine-toon-outline-03

优点:

  • 线条总是干净整齐的,因为描边是由多边形组成的。

  • 描边厚度很容易通过移动顶点来调整。

  • 描边会随着(模型离相机的)距离而缩小。这可能也是一个缺点。

缺点:

  • 通常,不会去画出内部网格三角形的描边细节。

  • 由于描边是由多边形组成的,所以很容易被遮挡裁剪掉。可以在上面的例子中看到,复制网格与地面重叠。

  • 可能会影响性能。这取决于网格有多少个多边形。因为你使用的是复制网格,所以你的多边形数量基本上翻了一倍。

  • 这种描边在光滑的凸多边形上表现得会好一些,但是在硬边和凹多边形上会产生洞。你可以在下面的图片中看到。

unreal-engine-toon-outline-03

一般来说,你应该在建模软件中创建反转的网格。这会让你更好地控制剪影。如果使用蒙皮骨骼模型,它还允许你将复制的网格蒙皮到原始的骨骼上,这样复制网格就会和原来的网格一起移动了。

在本教程中,你将在虚幻中创建网格而不是通过建模软件。方法略有不同,但是概念是一样的。

首先,你需要为复制网格创建材质。

创建反转网格的材质

用这个方法,你将屏蔽掉面朝外部的多边形,留下面朝内部的多边形。

注意:由于用到了遮罩,这种方法比使用手动创建的网格在性能上稍微昂贵一些。

进入Materials文件夹并打开M_Inverted。然后,进入“细节”面板并且调整以下设置:

  • 混合模式:设置为“已遮罩”。这将允许你将一些区域标记为可见的还是不可见的。可以通过编辑“不透明蒙版”来调整阈值。

  • 着色模型: 设置为“无光照”。这样灯光就不会影响网格了。

  • 双面:设置为启用。默认情况下,虚幻会剔除背面。启用此选项将禁用背面剔除。如果启用背面剔除,将无法看到面朝内部的多边形。

unreal-engine-toon-outline-04

接下来,创建一个Vector Parameter并且命名为OutlineColor,用它来控制描边的颜色。把它连接到自发光颜色

unreal-engine-toon-outline-05

为了屏蔽面朝外部的多边形,需要创建一个TwoSidedSign并将其乘以-1,并将其结果连接到不透明遮罩

unreal-engine-toon-outline-06

TwoSidedSign将正面输出1,背面输出-1。连接到不透明遮罩意味着正面可见,背面不可见。但是,你希望得到相反的效果。要执行此操作,可以乘以-1来反转符号。现在正面将输出-1,背面输出1

最后,你需要一种控制描边厚度的方法。为此,可以添加以下高亮显示的节点:

unreal-engine-toon-outline-07

在虚幻中,可以使用“世界场景位置偏移”移动每个顶点的位置。通过用顶点法线乘以OutlineThickness,可以改变网格厚度。以下是使用原始网格的演示:

unreal-engine-toon-outline-04

此时,材质已经完成。点击“应用”,然后关闭M_Inverted

现在,需要复制网格并应用刚刚创建的材质。

复制网格

进入Blueprints文件夹并打开BP_Viking。将静态网格体组件添加到Mesh节点下作为其孩子,并命名为“Outline”

unreal-engine-toon-outline-08

确保选中Outline并将其静态网格体设置为SM_Viking。然后,设置材质MI_Inverted

unreal-engine-toon-outline-09

MI_InvertedM_Inversed的一个实例。它允许你调整OutlineColorOutlineThickness两个参数而无需编译。

点击“编译”,然后关闭BP_Viking。现在模型viking就会有一个描边了。可以通过打开MI_Inverted并调整参数来控制描边的颜色和厚度。

unreal-engine-toon-outline-05

这就是通过反转网格创建描边的方法!思考一下是否可以在建模软件中创建一个反转法线的网格,然后把它导入虚幻来使用。

如果要用不同的方法来创建描边,可以使用后期处理

后期处理描边

你可以使用边缘检测来创建后期处理描边。这是一种检测图像中各区域间不连续的技术。以下是你可以查找到的几种不连续性:

unreal-engine-toon-outline-10

优点:

  • 能够很简单地应用到整个场景。

  • 性能消耗固定,因为Shader总是为每一个像素运行。

  • 无论离相机远近,描边线的粗细总是保持一致。这可能也会成为缺点。

  • 由于是后期处理产生的,描边不会被几何体裁剪掉。

缺点:

  • 通常需要多个边缘检测器来捕捉边缘,会影响性能。

  • 容易被干扰,这意味着描边将出现在颜色差异较大的地方。

进行边缘检测的常用方法是对每一个像素做卷积

什么是卷积

在图像处理中,卷积是对两组数字进行运算以产生耽搁数字。首先,将一个数字组成的网格(称作卷积核)的中心放在每一个像素上。下面是一个3×3卷积核在图像顶部两行上移动的示例:

unreal-engine-toon-outline-06

对于每个像素,将每个卷积核条目与其对应的像素相乘。让我们错嘴的左上角取像素进行演示。我们还将图像转换为灰度以简化计算。

unreal-engine-toon-outline-11

首先,放置卷积核(我们将使用与之前相同的卷积核),使目标像素位于中心。然后,将每个卷积核元素与其覆盖的像素相乘。

unreal-engine-toon-outline-12

最后,将所有结果相加。中心像素将会得到一个新的值。在这个例子中,新的值是0.5+0.5也就是1。以下是对每个像素执行卷积后的图像:

unreal-engine-toon-outline-convolutionresult

你使用的卷积核决定了最终得到的效果。图中的卷积核用于边缘检测。以下是其他卷积核的几个示例:

unreal-engine-toon-outline-13

注意:你会注意到,在图片编辑软件中可以找到这些滤镜。卷积实际上是图片编辑软件实现滤镜的原理。事实上,你可以在Photoshop中使用自己的卷积核执行卷积运算!

要检测图像中的边缘,可以使用拉普拉斯边缘检测(Laplacian edge detection)。

拉普拉斯边缘检测

首先,拉普拉斯边缘检测的卷积核是什么?它实际上就是你在上一节的例子中看到的那样!

unreal-engine-toon-outline-14

该卷积核可以用于边缘检测,因为拉普拉斯算子能测量斜率的变化。偏离0越大的区域,越表明它是一条边。

为了帮助你理解它,我们来看一下一维的拉普拉斯卷积。卷积核如下:

unreal-engine-toon-outline-15

首先,将卷积核放到边缘像素上,然后执行卷积。

unreal-engine-toon-outline-16

你将得到一个值1,表示这里发生了很大的变化,意味着目标像素可能是边缘。

接下来,让我们卷积一个变化幅度较小的区域。

unreal-engine-toon-outline-17

即使这些像素都有着不同的值,但是渐变是线性的,这意味着斜率没有变化,并且表明目标像素不是边缘。

下面是执行了卷积后的图像和绘制了每个值的图表。你可以看到边缘的像素离0更远。

unreal-engine-toon-outline-18

啊,好复杂的理论啊!但是不要担心——有意思的部分现在来了。在下一节中,你将构建一个在深度缓冲区上执行拉普拉斯边缘检测的后期处理材质。

构建拉普拉斯边缘检测

进入Maps文件夹并且打开PostProcess关卡。你会看到一个黑色的屏幕。这是因为场景包含了一个使用了空的后期处理材质的“后期处理体积”。

unreal-engine-toon-outline-19

这是你将要去编辑的材质,用它去构建边缘检测的材质。第一步是找出如何采样相邻像素。

要得到当前像素的位置,可以使用TextureCoordinate节点。例如,如果当前像素在图片的正中间,它将返回(0.5, 0.5)。这个二维向量叫做UV

unreal-engine-toon-outline-20

要采样不同的像素,只需要为“TextureCoordinate”加一点偏移。在100×100的图片中,每个像素在UV空间中的大小为0.01。要采样右侧的像素,可以在向量的X轴上加上0.01。

unreal-engine-toon-outline-21

然后,这有一个问题。随着图片分辨率的变化,像素大小也随之变化。如果在200×200图片中还使用相同的偏移量(0.01,0),它将采样右侧的两个像素。

要解决这个问题,你可以使用SceneTexelSize节点来返回像素大小。可以按照如下图所示操作来使用它:

unreal-engine-toon-outline-22

由于你要采样多个像素,因此必须多次创建这些节点。

unreal-engine-toon-outline-23

显然,这个蓝图很快就会变得很乱。幸运的是,你可以使用材质函数来保持蓝图的整洁。

注意:材质函数类似于蓝图和C++中的函数。

在下一节中,你将把重复的节点放入函数中,并创建一个偏移量的输入参数。

创建采样像素的函数

首先,进入“Materials\PostProcess”文件夹。要创建材质函数,点击添加/导入并且选择“材质和纹理/材质函数”

unreal-engine-toon-outline-24

重命名为“MF_GetPixelDepth”并且打开。蓝图里将只会有一个FunctionOutput节点。这就是你将要连接采样像素值的地方。

unreal-engine-toon-outline-25

首先,你需要创建一个接收偏移量的输入。为此,创建一个FunctionInput节点。

unreal-engine-toon-outline-26

当稍后使用该函数时,它将会显示为输入引脚。

现在你需要为输入指定一些设置。确保已经选择FunctionInput,然后转到“细节”面板,调整以下设置:

  • 输入名称:Offset。

  • 输入类型:函数输入向量2。由于深度缓冲区时一张2D图片,因此偏移值需要是一个向量2

  • 将预览值用作默认值:开启。如果你不提供输入值,那么函数将使用来自预览值的值。

unreal-engine-toon-outline-27

接下来,你需要将Offset乘以像素尺寸。然后,你需要你需要将结果和“TextureCoordinate”相加。为此,添加上如下图中所示的高亮的那些节点:

unreal-engine-toon-outline-28

最后,你需要使用提供的UV来采样深度缓冲区。添加一个SceneDepth节点并且如下图一样连接所有节点:

unreal-engine-toon-outline-29

注意:你也可以使用SceneTexture(设置场景纹理ID场景深度)来代替SceneDepth

总结:

  1. Offset采用向量2,并且乘以ScenetexlSize。你获得一个UV空间中的偏移量。

  2. TextureCoordinate加一个偏移量,以获得离当前像素(x, y)像素那么远的像素。

  3. SceneDepth将使用提供的UV采样合适的像素,并且接下来就输出它。

这就是材质函数。点击应用,然后关闭MF_GetPixelDepth

注意:你可能会在“统计数据”面板看到一条报错——“only translucent or post process materials can read from scene depth.”。你可以放心地忽略它。由于你将在后期处理材质中使用到该功能,因此它会起作用的。

接下来,你需要用函数在深度缓冲区上执行卷积。

执行卷积

首先,你需要为每个像素创建偏移值。由于卷积核四个角上的值都为0,你可以跳过它们。这样就只剩下上、下、左、右像素了。

打开PP_Outline并且创建四个Constant2Vector节点。如下设置它们:

  • (-1, 0)

  • (1, 0)

  • (0, -1)

  • (0, 1)

unreal-engine-toon-outline-30

接下来,你需要用卷积核采样五个像素。创建五个MaterialFunctionCall节点,并且设置MF_GetPixelDepth给每一个节点。然后,将每个偏移值连接到它们自己的函数上。

unreal-engine-toon-outline-31

你将获得每个像素对应的深度值。

接下来是乘法阶段。由于卷积核里锚点相邻的像素都是1,你可以跳过相乘。然而,你仍然需要将中心像素(最下面的那个函数)乘以-4。

unreal-engine-toon-outline-32

接下来,你需要将所有值加起来。创建四个Add节点,如下图一样连接它们:

unreal-engine-toon-outline-33

如果你还记得之前的每个像素值的图表,你会看到其中一些值是负数。如果按原样使用材质,数值为负的像素会显示为黑色,因为它们比零小。要解决这个问题,可以获得绝对值来将所有的输入转换成一个非负的值。添加一个Abs节点,如下图一样连接它们:

unreal-engine-toon-outline-34

总结:

  1. MF_GetPixelDepth节点会获取到中心、上、下、左、右像素的深度值。

  2. 将每一个像素乘以对应的卷积核数值。在这个例子里,你仅需要去乘中心像素。

  3. 计算所有像素的和。

  4. 获得值之和的绝对值。这会防止值为负的像素显示为黑色。

点击应用,然后回到主编辑器界面。现在整个画面充满了线条!

unreal-engine-toon-outline-07

不过这也有一些问题。首先,有些边缘只有轻微的深度差异。第二,由于背景是球体,因此它由一些圆形的线条。如果要将边缘检测和网格隔离开,这本身不是问题。但是,假如你想要为整个场景描边,那么那些圆圈并不是我们想要的。

要解决这些问题,可以用阈值来实现。

实现阈值化

首先,你将修复由于小深度差异而出现的线条。返回“材质编辑器”并创建下面的节点。确保Threshold设置为4

unreal-engine-toon-outline-35

稍后,将边缘检测的结果连接到A。如果像素值大于4,则输出1(表示边缘)。否则,输出0(不是边缘)。

接下来,您将去掉背景中的线条。创建下面的节点。确保将DepthCutoff设置为9000

unreal-engine-toon-outline-36

如果当前像素的深度大于9000,则输出0(不是边缘)。否则,它会输出A<B的值。

最终,像下图一样连接所有节点:

unreal-engine-toon-outline-37

现在,仅当像素值高于4阈值)且深度低于9000深度截断)时,才会显示线条。

点击应用,然后回到著编辑器节点。细碎的线条和背景上的线条都消失了!

unreal-engine-toon-outline-08

注意:你可以创建PP_Outline的材质实例来控制阈值深度截断

边缘检测运行表现得很好。但是如果想要更粗的线条呢?为此,你需要更大的卷积核大小。

创建更粗的线条

通常,较大的卷积核对性能的影响较大。因为你必须采样更多像素。但是,如果想要有一种方法可以使更大的卷积核具有与3×3卷积核相同的性能,该怎么办?扩张卷积就派上用场了。

在扩张卷积中,只需将偏移间隔得更远。为此,将每个偏移量乘以一个叫做扩张率(dilation rate)的标量。这定义了每个卷积核元素之间的间距。

unreal-engine-toon-outline-09

如您所见,这样就允许你在采样相同数量的像素时增加卷积核大小了。

现在让我们一起来实现扩张卷积。返回到“材质编辑器”,创建一个叫做DilationRateScalarParameter,将其值设置为3。然后,将每个偏移量乘以DilationRate

unreal-engine-toon-outline-38

这会让每个偏移离中心像素有3个像素远。

点击应用,然后回到主编辑器界面。你会看到你的线条变粗不少。这里是不同扩张率(dilation rate)之间的比较:

unreal-engine-toon-outline-10

除非你就是想要为你的游戏找一种纯线条风格的艺术,否则你可能还是想在原始场景里显示描边。在最后一节中,你将向原始场景的图像中添加描边线条。

添加描边线条到原始图像上

返回“材质编辑器”并创建下面的节点。这里的顺序很重要!

unreal-engine-toon-outline-39

接下来,像下图一样连接所有节点:

unreal-engine-toon-outline-40

现在,如果alpha达到0(黑色),Lerp将输出场景图像。否则,它将输出LineColor

点击应用,然后关闭PP_Outline。原始场景现在就会有描边了!

unreal-engine-toon-outline-41

接下来干什么?

如果你想在边缘检测方面做更多的工作,可以尝试创建一个在普通缓冲区上工作的边缘检测。这将为你提供一些深度边缘检测器中未显示的边缘。然后可以将这两种类型的边缘检测组合在一起。

卷积是一个广泛的主题,有许多用途,包括人工智能和音频处理。我鼓励你通过创建其他效果(如锐化和模糊)来探索卷积。其中一些操作非常简单,只需更改卷积核中的值即可!查看《Images Kernels explained visually》,了解卷积的在人机交互上的解释。它还包含一些其他效果的卷积核。

我还强烈建议你去看GDC关于《Guilty Gear Xrd’s art style》的演讲。它们还使用反转网格法来处理外部描边。然而,对于内部描边,他们使用纹理和UV操作提供了一种简单而巧妙的技术。


Similar Posts

评论