使用Unity着色器实现精灵(Sprite)涂鸦效果
本文将由来自英国的游戏开发工程师Alan Zucconi分享如何在Unity中使用着色器制作近来流行的精灵涂鸦效果。
精灵涂鸦效果在过去几年逐渐流行起来,《GoNNER》和《Baba is You》等游戏大量使用了这种美术效果。
本文将展示在无需绘制多个不同图像的情况下,如何实现精灵涂鸦效果。本文将介绍从Unity着色器编程的基础到所应用的数学原理等所有必要知识。
引言
本文会涉及一些比较高级的话题,包括:反向运动学的数学原理和大气的瑞利散射效果。但是既对这些内容感兴趣,又有理解所需的必备技术知识的开发者其实并不多。
在游戏开发者Nick Kaman的一则推文中,他展示了如何在Unity实现涂鸦效果。
Nick Kaman:我想分享一个在Unity实现“涂鸦”效果的技巧:
我们不必绘制相同精灵的不同帧,我们可以把精灵放到网格,然后使用法线贴图偏移顶点即可,该法线贴图会每秒X次大幅进行滚动。
这篇推文获得大量的点赞和转发,我们发现,让即使没有着色器编程知识的人也可以理解的简单教程是很有必要的。
如果想要制作2D精灵动画的专业而高效的方法,并且需要完整的艺术级控制功能,你可以使用Doodle Studio 95!资源。
获取Doodle Studio 95!:
https://fernandoramallo.itch.io/doodle-studio-95
下面是使用Doodle Studio 95!时的动画图片。
涂鸦效果的剖析
为了实现涂鸦效果,我们首先需要理解实现原理以及使用了哪些技术。
着色器效果
首先,我们想要涂鸦效果尽可能轻量,不使用任何额外脚本。我们可以通过着色器实现这种效果,指导Unity在屏幕上渲染3D模型或者平面模型。
精灵着色器
Unity提供了多种着色器的类型,如果使用Unity提供的2D工具,开发者可能想要处理精灵。在这种情况下,你需要使用精灵(Sprite)着色器,它是一种特殊类型的着色器,与Unity的SpriteRenderer兼容。此外,你也可以使用较为传统的Unlit着色器。
顶点替换
在手动绘制精灵时,不会有相同的两个帧。我们想要通过使精灵进行“摇晃”,模拟出这种效果。使用着色器有一种非常高效的实现方法,该方法需要使用顶点替换功能。这种方法可以修改3D对象的顶点位置。如果随机变化这些位置,我们就可以实现想要的效果。
对齐时间
手绘动画通常有较低的帧率,如果我们想要模拟出诸如每秒5帧的画面,我们需要每秒5次修改精灵的顶点位置。但是,Unity可能会在更高刷新速率下运行游戏,可能会有每秒30帧或60帧的帧率。为了确保我们的精灵不以每秒60次的速度发生变化,我们需要处理动画的时间组件。
扩展精灵着色器
如果想要在Unity创建新的着色器,我们可以使用Unlit Shader,尽管它不一定是特定应用程序的最佳选择。
如果想让涂鸦着色器完全兼容Unity的SpriteRenderer,我们需要扩展它的现有精灵着色器。但是,在Unity中无法直接获取该着色器。
获取该着色器的方法是:访问Unity下载存档页面,下载正在使用Unity版本的Build in shaders资源包,该Zip压缩文件包含特定Unity版本推出的所有着色器源代码。
下载Build in shaders资源包:
https://unity3d.com/get-unity/download/archive
下载完成后,提取文件,然后在builtin_shaders-2018.1.6f1\DefaultResourcesExtra文件夹内找到Sprites-Diffuse.shader文件,它就是我们在本文中需要使用的文件。
如果Sprites-Diffuse文件不是默认的精灵着色器,该怎么办?
在创建新的精灵时,默认材质使用的着色器名为Sprites-Default.shader,而不是Sprites-Diffuse.shader。
两者的区别在于:前者是无光着色器,而后者会对场景的光线做出反应。由于Unity的实现方法,相对无光着色器,漫反射着色器可以更简单地进行编辑。
顶点替换功能
在Sprites-Diffuse.shader文件中,有一个称为vert的函数,它就是之前提到的顶点函数。它的名称并不重要,只要它符合#pragma指令的“vertex: ”部分内的名称即可。
#pragma surface surf Lambert vertex:vert nofog nolightmap nodynlightmap keepalpha noinstancing
简单来说,顶点函数会在3D模型的每个顶点调用,并决定如何在2D屏幕空间进行映射。对于本文而言,我们仅对理解如何替换对象感兴趣。
参数appdata_full v包含名为vertex的字段,该字段包含对象空间中每个顶点的3D位置,修改它的数值会移动顶点。
例如:下面的代码会使用该着色器把对象沿着X轴平移一个单位。
void vert (inout appdata_full v, out Input o)
{
v.vertex = UnityFlipSprite(v.vertex, _Flip);
v.vertex.x += 1;
#if defined(PIXELSNAP_ON)
v.vertex = UnityPixelSnap (v.vertex);
#endif
UNITY_INITIALIZE_OUTPUT(Input, o);
o.color = v.color * _Color * _RendererColor;
}
默认情况下,使用Unity制作的2D游戏仅处理X轴和Y轴,因此我们需要修改v.vertex.xy,从而在2D平面上移动精灵。
什么是对象空间?
结构appdata_full的vertex字段包含着色器在对象空间处理的当前顶点位置,如果对象处于游戏世界的中心点即(0,0,0)坐标,它就是该对象未经过缩放和旋转时顶点的位置。
相对地,在世界空间表示的顶点会反映顶点在Unity场景内的实际位置。
为什么对象不会以每帧1米的速度移动?
如果对C#脚本的Update方法内transform.position的x部分加1,我们会看到对象以每帧1个单位速度飞行,换算的速度约为每小时216千米。
发生这种情况是因为C#对位置的改动会改变位置本身。在顶点函数中,这种情况不会发生,着色器仅会改变模型的视觉效果,但不会更新或改变模型上已保存的顶点,因此给v.vertex.x添加+1仅会每次移动对象1米的距离。
别忘了以Tight类型导入精灵。
该效果会替换精灵上的顶点。传统情况下,精灵会作为四边形(即下图左侧)导入Unity。这意味着精灵仅有4个顶点。如果是这样,只有这些顶点可以进行移动,从而会减少涂鸦效果的总体强度。
为了实现更为紧密和逼真的扭曲效果,我们应该确保精灵以Mesh Type设为Tight的情况进行导入,这样会把精灵包装为凸面外壳(即下图右侧)。
这样做会提高顶点的数量,虽然这不总是理想的选择,但却是我们所需要的。
随机的替换效果
涂鸦效果会随机改变每个顶点的位置。在着色器采样随机数字是一件需要技巧的事,这是由于GPU的无状态架构,它使模拟大多数库使用的相同算法变得更加困难和低效。
Nick Kaman提供的方法是使用噪声纹理,该纹理在采样时会得到随机的感觉。对我们的情况而言,这种方法可能不是最高效的方法,因为它会加倍着色器必须执行的纹理查询次数。
因此,许多着色器需要使用比较模糊和混乱的函数,即使它们的效果是确定的,而且在我们看来没有任何模式。
由于函数必须是无状态的,每个随机数必须通过其自带的种子代码来生成。这种方法的效果很好,因为每个顶点的位置都应该是独特的。我们可以使用它关联每个顶点的随机数,我们会在后面讨论这种随机函数的实现方法,现在我们把该函数称为random3。
我们可以使用random3函数生成每个顶点随机的替换效果。在下面例子中,随机数会通过_NoiseScale属性调整,这样可以控制替换效果的强度。
void vert (inout appdata_full v, out Input o)
{
...
float2 noise = random3(v.vertex.xyz).xy * _NoiseScale;
v.vertex.xy += noise;
...
}
现在我们要编写random3函数的代码。
着色器内的随机效果
着色器中最常用和最具标志性的伪随机函数来自W.J.J. Rey在1998年发表的论文。
float rand(float2 co)
{
return fract(sin(dot(co.xy ,float2(12.9898,78.233))) * 43758.5453);
}
该函数是确定性的,也就是说它不是真正具有随机效果,但是它的行为非常不规律,使它看起来完全是随机的,这类函数被称为伪随机函数。对于本教程,我使用了Nikita Miropolskiy编写的高级函数。
添加时间
通过使用已经编写好的代码,我们现在可以实现每个点都会在每帧替换相同的次数。这样会实现摇摆的精灵,而不是涂鸦效果。
为了解决该问题,我们需要找到随时间改变效果的方法,最简单的方法是使用顶点位置和当前时间来生成随机数。
在这种情况下,我们添加了以秒为单位的当前时间值_Time.y到顶点位置。
float time = float3(_Time.y, 0, 0);
float2 noise = random3(v.vertex.xyz + time).xy * _NoiseScale;
v.vertex.xy += noise;
更高级的效果需要更复杂的方法来集成时间到计算方程式中,但由于我们只想实现间隔的随机效果,因此添加两个数值就足够了。
对齐时间
添加_Time.y的主要问题是:它会造成精灵在每帧都发生变化。这是不理想的效果,因为大多数手绘的动画都有较低的帧率。
时间组件不应该有连续的效果,而是应该变得离散化,这意味着如果我们想实现每秒5帧,它应该仅在每秒改变5次。使用熟悉术语的话说,那就是:时间应该“对齐”为一秒的五分之一。因此,可以使用的数值应该为:0/5 = 0,1/5 = 0.2,2/5 = 0.4,3/5 = 0.6,4/5 = 0.8,5/5 = 1 ,以此类推。
下面的函数会接收数值x,对齐到Snap值的整数倍数。
inline float snap (float x, float snap)
{
return snap * round(x / snap);
}
因此,我们可以更新为以下代码:
float time = snap(_Time.y, _NoiseSnap);
float2 noise = random3(v.vertex.xyz + float3(time, 0.0, 0.0) ).xy * _NoiseScale;
v.vertex.xy += noise;
大功告成,最后的效果如下图所示。
小结
如何使用Unity着色器实现精灵涂鸦效果为大家介绍到这里,喜欢自定义着色器的朋友们不妨一试。
下载Unity Connect APP,请点击此处。观看更多Unity官方精彩视频,请关注“Unity官方”B站账户。
扫码在“技术交流“群聊组中提问,Unity社区和官方团队帮你解答。
推荐阅读
官方活动
喜欢本文,点击“在看”