找回密码
 立即注册
查看: 482|回复: 4

[译文]Unreal Engine 4 手绘风滤镜(Paint Filter)即 桑原滤镜(Kuwahara Filter)教程(上)

[复制链接]
发表于 2021-4-2 09:29 | 显示全部楼层 |阅读模式
原文|《Unreal Engine 4 Paint Filter Tutorial》
作者|Tommy Tran May 1 2018  | 翻译 开发游戏的老王 阅读时长|25分钟 内容难度|入门级 本教程将会教你通过实现桑原滤镜(Kuwahara Filter)让游戏具有手绘风格
时光流逝,电子游戏的视觉效果越来越好。在这个画面惊艳的游戏层出不穷的时代,让自己的游戏脱颖而出是件很困难的事情。然而有一种方法可以使你的游戏在视觉美学上与众不同,那就是非真实渲染(non-photorealistic rendering)。
非真实渲染的技术范畴非常广,它包含但不仅限于卡通着色,卡通描边以及交叉线。你甚至可以让你的游戏看起来更具手绘风。技术之一就是实现桑原滤镜(Kuwahara Filter)。
要实现桑原滤镜,你将会学到:
    如何为多核(Kernel)计算平均值(mean)和方差(variance)输出具有最小方差的核的平均值使用索贝尔算子(Sobel)获取像素的局部朝向(local orientation)基于像素的局部朝向旋转采样核(sampling kernel)
注:本文假定读者已经熟悉Unreal Engine的基础知识。因为本文将使用HLSL,所以你应该熟悉HLSL或至少熟悉一种诸如C#的语言。
本文是Unreal Engine着色器教程四部曲之一:
    第一部分: 卡通着色(Cel Shading)

    第二部分:卡通风轮廓线(Toon Outline)


    第三部分:使用HLSL自定义着色器(Custom Shaders Using HLSL)


    第四部分:手绘风滤镜(Paint Filter)即桑原滤镜(Kuwahara Filter)【本文】
开始吧

译者注: 本教程提供了范例工程,如果需要可以到原文网站免费注册并下载
下载工程文件并解压缩,找到PaintFilterStarter文件夹,打开PaintFilter.uproject,你就会看到下面的场景:


为了节省大家的时间,这个场景已经包含了自定义节点PP_KuwaharaPost Process Volume ,这个材质(以及其着色器),就是我们要编辑的。


那么,我们先来了解一下桑原滤镜和它的工作原理吧。
桑原滤镜

照相的时候,我们有时会在照片表面发现一种颗粒状的纹理。就如同来自我们吵闹的邻居家中的噪声一样,我们往往不喜欢它。




常用的去噪方法就是使用诸如模糊之类的低通滤镜(low-pass filter ),下图就是使用半径为5的矩形模糊算法处理过的图片。




如上图所示,噪声基本被消除了,但是所有的边缘也变得模糊了。有没有一种方法能再平滑图像的同时保持边缘呢。
如你猜测的那样,桑原滤镜就能够满足这个需求,让我们看看它是如何工作的。
桑原滤镜的工作原理

和卷积运算一样,桑原滤镜也要使用核(kernel),不同的是它不是使用1个而是4个。4个核如下图(5×5的桑原滤镜)所示布局,每两个核之间会有1个像素重叠(当前像素)




首先,计算每一个核中颜色的平均值,这步是为了后面要通过模糊来降噪。
对于每个核,还要计算方差,这主要是测量每个核中颜色值的变化大小。如果一个核中的颜色很相近,那么这个方差会很小;反之,如果颜色不相近,那么方差会很大。
最后,我们找到方差最低的核,并输出它的颜色平均值。这种基于方差的选择就是桑原滤镜保持边缘的方法。让我们看几个例子。
桑原滤镜的例子

下面是个10×10的灰度图,我们可以看到一条从左下到右上的边缘,以及某些区域存在的噪声。




首先,选择一个像素并计算出方差最小的核。下图为本例所选的一个边缘附近的像素以及其相关的核:


如你所见,在边缘上的核,其颜色是抖动的,这意味着高方差因此它们也不会被选择。也正因此,桑原滤镜防止了边缘的模糊。
本例中,滤镜会选择绿色框框的那个核,因为它最稳定。于是输出的结果就是这个核中颜色的平均值(有点接近黑色)。
下面为另一个像素的以及它的核:




这次,黄色框框的核拥有最低的方差,因此它是唯一一个不在边缘的。所以本次输出结果为黄色核中颜色的平均值,这次结果有点接近白色。
下图为矩形模糊核桑原滤镜的对比,它们的半径都是5.




如你所见,桑原滤镜在平滑且保持边缘方面做的相当不错,本例中它让边缘依然保持清晰!
同时,这个边缘保持的平滑特性还给图像带来了一种手绘风。因为,绘画的笔刷通常就是边缘较硬而且不会产生很大(画面)噪声的。于是,桑原滤镜就是一个将写实风转换成手绘风的不错的选择。
以下就是一个使用不同大小桑原滤镜处理照片的效果图




创建桑原滤镜

本教程中,滤镜将被分割成两个着色器文件:Global.usfKuwahara.usf。第一个文件要存储计算核平均值以及方差的函数;第二个文件要存储滤镜的入口并且将会为每个核调用上述函数。
首先,我们来定义计算平均值和方差的函数。在范例工程中找到Shaders文件夹,然后打开其中的 Global.usf。我们可以看到其中名为GetKernelMeanAndVariance()的函数。
开始构建函数之前,我们需要添加几个参数。修改后的函数如下:
float4 GetKernelMeanAndVariance(float2 UV, float4 Range)我们需要两重循环对一个网格进行采样:一个循环负责垂直方向另一个负责水平方向。Range参数的前两个通道存储水平循环的边界,后两个通道存储垂直循环的边界。例如:我们正在对坐上角的核进行采样且核大小为2,那么它的Range即为:
Range = float4(-2, 0, -2, 0);好,现在开始抽样。
像素抽样

先创建2重循环,在GetKernelMeanAndVariance()函数中添加如下代码:
for (int x = Range.x; x <= Range.y; x++)
{
    for (int y = Range.z; y <= Range.w; y++)
    {

    }
}于是,我们会得到核中所有(相对采样像素的)偏移量。例如:如果正在对左上角采样且半径为2,那么偏移量的范围将会是 (0, 0)(-2, -2)




现在,我们需要获取采样像素的颜色,将下面的代码添加到循环中:
float2 Offset = float2(x, y) * TexelSize;
float3 PixelColor = SceneTextureLookup(UV + Offset, 14, false).rgb;第一行是将采样像素的偏移量换算到UV空间,第二行是使用该偏移量获取被抽样像素的颜色。
接下来我们要计算平均值和方差了。
计算平均值和方差

计算平均值很简单,累加所有颜色值然后除以样本总数;对于方差,我们使用如下公式,其中x是采样像素颜色:




首先我们需要计算总和。对于平均值只要把每个颜色累加到Mean变量就可以了。对于方差,我们需要先对它求平方让后在累加到Variance变量中。将下面的代码添加到之前代码的下方:
Mean += PixelColor;
Variance += PixelColor * PixelColor;
Samples++;在for循环的下面添加如下代码:
Mean /= Samples;
Variance = Variance / Samples - Mean * Mean;
float TotalVariance = Variance.r + Variance.g + Variance.b;
return float4(Mean.r, Mean.g, Mean.b, TotalVariance);前两行代码计算平均值和方差,但这里存在一个问题:方差有RGB三个分量,所以,干脆把它们加到一起以得到一个总方差
最终,该函数以float4的形式分会平均值和方差。平均值占用RGB三个通道,而方差占用A通道。
现在我们拥有了计算平均值和方差的函数,我们需要对每一个核调用它。回到Shaders文件加并打开Kuwahara.usf。首先,你要创建几个变量:
float2 UV = GetDefaultSceneTextureUV(Parameters, 14);
float4 MeanAndVariance[4];
float4 Range;解释一下每一个参数“
    UV: 即每个像素的UV坐标MeanAndVariance: 存有每一个核的平均值和方差的数组Range: 存储当前核每重循环边界值
接下来,为每一个核调用GetKernelMeanAndVariance()函数:
Range = float4(-XRadius, 0, -YRadius, 0);
MeanAndVariance[0] = GetKernelMeanAndVariance(UV, Range);

Range = float4(0, XRadius, -YRadius, 0);
MeanAndVariance[1] = GetKernelMeanAndVariance(UV, Range);

Range = float4(-XRadius, 0, 0, YRadius);
MeanAndVariance[2] = GetKernelMeanAndVariance(UV, Range);

Range = float4(0, XRadius, 0, YRadius);
MeanAndVariance[3] = GetKernelMeanAndVariance(UV, Range);这段代码使我们按照如下顺序获得每一个核的平均值和方差:top-left, top-right, bottom-left 以及 bottom-right。
接下来,我们选择方差最小的核并输出其平均值。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2021-4-2 09:33 | 显示全部楼层
不过这种风格还是在贴图阶段就做好手绘感比较好吧?渲染负担大。
发表于 2021-4-2 09:35 | 显示全部楼层
和真手绘的效果是有差距的,主要针对3D游戏的后期渲染,贴图阶段实现不了,尤其是边缘
发表于 2021-4-2 09:36 | 显示全部楼层
贴图边缘可以全上用alpha贴图、粒子,不过这样子负担也不小,怪不得世面上类似的都是硬边扁平风
发表于 2021-4-2 09:45 | 显示全部楼层
哇!谢谢大佬关注,我现在菜的一批,会好好努力的!
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 20:41 , Processed in 0.093816 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表