游戏中水彩效果的实现

图片

介绍


当我们在2019年1月开始讨论新的色彩游戏时,我们立即决定水彩效果将是最重要的元素。受到宝格丽(Bulgari)广告的启发,我们意识到水彩画的实现应与我们计划创造的剩余资源的高质量保持一致。我们从Adobe的研究人员那里找到了一篇有趣的文章(1)。其中描述的水彩画技术看起来很棒,并且由于其矢量(而非像素)性质,即使在较弱的移动设备上也可以使用。我们的实施基于此研究,我们更改和/或简化了部分内容,因为我们的性能要求不同。色彩-这是一款游戏,因此,除了图形本身之外,我们还需要渲染整个3D环境并在一帧中执行游戏逻辑。我们还试图确保模拟是实时执行的,并且玩家可以立即看到绘制的内容。


色彩中的水彩模拟。

在本文中,我们将分享在Unity游戏引擎中实施此技术的各个细节,并讨论如何使它适应于低端移动设备上的无缝运行。我们将更多地讨论该算法的主要阶段,但不演示代码。该实现是在Unity 2018.4.2中创建的,后来又更新为版本2018.4.7。

什么是色调?


色调-这是一款益智游戏,可让玩家完成各个级别,混合水彩的颜色,使其与折纸的颜色匹配。该游戏于2019年秋季在Apple Arcade发行,适用于iOS,macOS和tvOS。


屏幕截图色调。

要求


我的文章中描述的技术可以分为在每个框架中执行的三个主要阶段:

  1. 根据玩家的输入生成新的景点并将其添加到景点列表中
  2. 绘制列表上所有点的模拟
  3. 点渲染

下面我们将详细讨论如何实现每个阶段。

我们的目标是达到60 FPS,也就是说,这些阶段以及下面描述的所有逻辑每秒执行60次。

获取输入


在每一帧中,我们将玩家的输入(取决于平台,可以是触摸,鼠标或虚拟光标的位置)转换成splatData结构,该结构包含位置,运动矢量,颜色和压力(2)。首先,我们在屏幕上检查玩家的划动长度,并将其与给定的阈值进行比较。轻扫一下,我们在输入位置每帧生成一个点。在相反的情况下,我们用预定密度创建的新点填充玩家滑动的起点和终点之间的距离(这确保了恒定的绘画密度,而与滑动速度无关)。颜色表示当前使用的绘画,移动的坡度是滑动的方向。创建的新点将添加到名为splatList的集合中,其中还包含所有先前创建的位置。在以下步骤中,它用于模拟和渲染涂料。每个单独的点都表示需要渲染的一滴油漆-水彩画的主要构成部分。完成的水彩画将是渲染数十个/数百个相交点的结果。此外,将生命周期值(以帧为单位)分配给新创建的斑点,该斑点确定可以模拟斑点多长时间。


长滑动点插值的示例。空心圆圈表示以固定间隔创建的斑点。

帆布


像真正的油漆一样,我们需要一块画布。为了实现它,我们在3D空间中创建了一个看起来像一张纸的有限区域。玩家的输入坐标和所有其他操作(例如渲染网格)都记录在画布空间中。同样,用于模拟绘图的任何缓冲区的像素大小取决于画布的大小。本文中使用的术语“画布”与Unity UI中的Canvas类没有任何关系。


绿色矩形显示游戏中的画布区域


在视觉上,该点由圆形网格表示,该网格的边缘包含25个顶点。如果您在很短的时间内触摸它,您可以将其看作是湿画笔在纸上留下的“滴”。我们向每个顶点的位置添加一个小的随机偏移量,以确保油漆斑点边缘的不均匀性。


网格物体的例子。

对于每个顶点,我们还存储向外的速度矢量,然后将其用于仿真阶段。我们生成了几个这样的网格,它们之间的形式略有不同,并将它们的数据存储在skriptuemy对象(可编写脚本的对象)中。每次玩家实时绘制一个点时,我们都会为其分配一个从该组中随机选择的网格。值得一提的是,在不同的屏幕分辨率下,画布的像素大小也不同。因此,在所有设备上,斑点的大小系数都是相同的,因此当您开始游戏时,我们会根据画布的大小来更改比例。


存储有新斑点数据的斑点向量的示例。

当生成斑点网格时,我们还保存了其“润湿区域”,该区域定义了原始斑点边界内的一组像素。润湿区域用于模拟对流。在创建每个新点时,在应用程序执行期间,我们将其下面的画布标记为湿的。模拟油漆的运动时,我们允许其“散布”在已经变湿的画布区域上。我们将画布的水分含量存储在全局湿图缓冲区中,该缓冲区在添加每个新点时进行更新。除参与两种颜色的混合外,平流在油漆笔划本身的最终外观中也起着重要作用。


湿图填充(斑点形状(绿色圆圈)内的像素)将湿缓冲区(网格)标记为湿(绿色)。Wetmap缓冲区本身具有更高的分辨率。

此外,每个斑点还包含一个不透明度,该是其面积的函数;它代表储存颜料的效果(斑点中一定数量的颜料)。当在仿真过程中增加斑点的大小时,其不透明度会降低,反之亦然。


一个不带有平流的涂料的示例(左),一个带有平流的涂料(右)。


油漆平流的例子。

仿真周期


收到玩家在当前帧中输入的内容并将其转换为新的斑点后,下一步是模拟斑点以模拟水彩的散布。在此模拟的开始,我们有一个需要更新的点列表和一个更新的wetmap

在每一帧中,我们遍历斑点列表并使用以下公式更改斑点所有顶点的位置:


其中:m是新的运动矢量,a是常数校正参数(0.33),b是运动斜率矢量=玩家挥杆的标准化方向乘以0.3,cr是画布粗糙度的标量值= Random.Range(1,1 + r),r是全局粗糙度参数,对于标准涂料,我们将其设置为0.4,v是预先通过点网格创建的速度矢量,vm是速度因数,在某些情况下我们在局部用于加速对流的标量值x(t + 1) -潜在的新顶点位置,x(t) -当前顶点位置,br是分支粗糙度向量=(Random.Range(-r,r),Random.Range(-r,r)),w(x)是湿图缓冲区中的润湿值。

这样的方程式的结果称为有偏随机游动,它模仿了真实水彩颜料中粒子的行为。我们试图将光斑的每个顶点从其中心(v向外移动,以增加随机性。然后,运动方向随冲程方向(b略有变化,并再次由另一个粗糙度分量(br随机化。然后将此新的顶点位置与一个湿贴图进行比较。如果新位置的画布已经湿透了(湿贴图缓冲区中的值大于0),则为顶点赋予新位置x(t + 1),否则不更改其位置。结果,油漆将仅在已经湿透的画布区域中扩散。在最后阶段,我们重新计算斑点区域,该斑点区域在渲染周期中用于更改其不透明度。


油漆两个有效点之间对流模拟的微型示例。

渲染周期-湿缓冲区


重新计算斑点后,即可开始渲染它们。在仿真阶段之后的出口处,斑点的网格经常被证明是变形的(例如,发生相交),因此,为了正确渲染而无需重复三角剖分的额外费用,我们使用了带有两遍模板缓冲区的解决方案。 Unity Graphics绘图界面用于渲染斑点,并且渲染周期在Unity OnPostRender方法内部执行使用单独的相机渲染点网格以渲染纹理(wetBuffer)。在周期的开始,使用Graphics.SetRenderTarget(wetBuffer)清除wetBuffer并将其设置为渲染目标splatList中每个活动点的下一个 我们执行下图所示的序列:


渲染周期图。

我们首先在每个斑点之前清洗模板缓冲区,以使先前斑点的模板缓冲区的状态不会影响新斑点。然后,我们选择用于绘制斑点的材料。该材料负责点的颜色,当玩家绘制点时,我们根据splatData中存储的颜色索引选择它然后我们根据上一步中计算的点网格的面积来更改颜色不透明度(alpha通道)。渲染本身是使用两遍模板缓冲区着色器执行的。在第一遍(Material.SetPass(0))中,我们传递原始点网格以记录填充网格的坐标。通过此通行证ColorMask分配的值为0,因此不会渲染网格物体。在第二遍(Material.SetPass(1))中,我们使用围绕点网格描述的四边形。我们检查模板缓冲区中四边形每个像素的值;如果值为1,则渲染像素,否则跳过该像素。作为此操作的结果,我们渲染了与点网格相同的形状,但是它肯定不会包含不需要的伪像,例如自相交。


执行双重模板缓冲技术的过程(从左到右)。请注意,此模板缓冲区的分辨率比显示的分辨率高得多,因此可以高精度地保持其原始形状。


以传统方式绘制的三个相交斑点的示例,这导致了伪像的出现(左),并使用了两遍模板缓冲技术消除了所有伪像(右)。

wetBuffer中渲染了所有斑点之后,它将显示在游戏场景中。我们的画布使用临时着色器,将wetBuffer,漫反射纸质贴图和纸质法线贴图结合在一起


画布着色器:仅wetBuffer(左),添加的纸张纹理(中),法线贴图(右)。

该游戏为有色盲的人提供了一种模式,其中将单独的图案叠加在颜料上。为此,我们通过添加带有平铺图案的纹理来更改污渍的材质。模式遵循混合游戏颜色的规则,例如,蓝色(长条)+黄色(圆)在交叉点处给出绿色(长条中的圆圈)。若要无缝混合图案,必须在相同的UV空间中渲染它们。我们调整模板缓冲区第二遍中使用的四边形的UV坐标,将x和y位置(在画布空间中指定)除以画布的宽度和高度。结果,我们在0到1的空间中获得了u,v的正确值。


色盲图案的示例。

优化-干斑缓冲


如上所述,我们的任务之一是支持低功耗移动设备。现场渲染成为我们游戏的瓶颈。每个斑点需要三个绘制调用(调用两次通过+清除模板缓冲区),并且由于绘画行包含数十个或数百个斑点,绘制调用的数量迅速增加并导致帧速率下降。为了解决这个问题,我们应用了两种优化技术:首先,同时绘制dryBuffer中所有“干燥”的斑点,其次,达到一定数量的活性斑点后,局部加速了斑点的干燥。

dryBuffer是添加到渲染周期的附加渲染纹理。如前所述,每个光斑都有一个寿命(以帧为单位),该寿命随每个帧而减少。寿命达到0后,污渍被认为是“干燥”的。不再模拟干点,它们的形状不会改变,因此不需要在每个帧中再次渲染它们。


DryBuffer起作用;灰色斑点表示复制到dryBuffer的斑点。

寿命达到0的每个点都将从splatList中移除,并“复制”到dryBuffer。在复制过程中,渲染周期被重用,这次将dryBuffer设置为目标渲染纹理仅通过重叠画布着色器中的缓冲区就无法实现wetBufferdryBuffer

之间的正确混合,因为wetBuffer缓冲区的渲染纹理包含已经用alpha值渲染的斑点(相当于预乘alpha)。通过在渲染遍历斑点之前在渲染周期的开始增加一个步骤,我们避免了此问题。此时,我们渲染显示出dryBuffer的照相机修剪金字塔的四边形因此,wetBuffer中呈现的任何污渍将已经与干燥的,先前涂过的污渍混合。


干点和湿点的混合物。dryBuffer

缓冲区会累积所有“干燥”的斑点,并且不会在帧之间清除。因此,与过期污渍相关的所有内存都可以在将它们“复制”到缓冲区后清除。


多亏了dryBuffer的优化,我们不再限制玩家可以应用到画布上的绘画数量。单独

使用dryBuffer技术可以使玩家绘画几乎无限量的绘画,但不能保证始终如一的性能。如上所述,绘画笔划具有恒定的厚度,这是通过在滑动的起点和终点之间使用许多点的插值进行绘制来实现的。在多次快速和长时间滑动的情况下,玩家可以生成大量活动点。这些斑点将在其寿命指定的帧数上进行仿真和渲染,最终导致更低的帧速率。

为了确保稳定的帧速率,我们更改了算法,以使活动点的数量受到maxActiveSplats恒定值的限制。所有超过该值的斑点都会立即“变干”。这是通过将最旧的活动斑点的寿命减少到0来实现的,这就是为什么将它们较早复制到干燥斑点缓冲区的原因。因为当我们缩短寿命时,我们会在模拟的不完整状态中找到一个位置(这看起来会很有趣),与此同时,我们提高了涂料的扩散速度。由于速度的增加,光斑的大小几乎达到了正常速度下的正常寿命。


演示最多40个(顶部)和80个(底部)活动点。在dryBuffer中复制的干斑显示为灰色。该值表示可以同时模拟的涂料“量”。maxActiveSplats

的值是最重要的性能参数,它使我们能够精确控制可分配给水彩渲染的绘画调用的数量。我们根据平台和设备功能在启动时进行设置。如果检测到帧速率降低,也可以在应用程序执行期间更改此值。

结论


该算法的实现已成为一项有趣且具有挑战性的任务。我们希望读者喜欢这篇文章。您可以在原始评论中提问如果您想欣赏我们的水彩作品,请尝试使用淡色。在Apple Arcade上


在Apple TV上运行的游戏的屏幕截图

(1)S. DiVerdi,A。Krishnaswamy,R。Mach和D. Ito,“用多边形绘画:程序水彩引擎”,在IEEE Transactions on Visualization and Computer Graphics,vol。1中。19号 5页。723–735,2013年5月。doi:10.1109 / TVCG.2012.295

(2)仅在用iPad绘制Apple Pencil时才考虑压力。

All Articles