前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Unity 水、流体、波纹基础系列(二)——方向流体(Directional Flow)

Unity 水、流体、波纹基础系列(二)——方向流体(Directional Flow)

作者头像
放牛的星星
发布2020-08-28 10:05:24
4.1K0
发布2020-08-28 10:05:24
举报
文章被收录于专栏:壹种念头壹种念头

本文重点: 对齐纹理和流体方向 把表面切割为瓦片 无缝混合瓦片 混淆视觉效果

这是流体材质的第二篇,继上一篇纹理变形之后,讲述如何对齐流体而不再是将它们进行扭曲。

本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。

该篇教程使用Unity 2017.4.4f1完成。

(顺其自然的涟漪)

1 各向异性模式

让纹理变形以模拟流动时,它最终可能在任何方向上被拉伸或挤压。这意味着无论如何变形,看起来效果都还不错。但这仅在各向同性模式下才有可能。各向同性意味着图像在所有方向上看起来都相似。我们在上一教程中使用的水纹理就是这种情况。

1.1 涟漪水

尽管流动的假象让人信以为真,但通过使各向同性图案变形而形成的图案看起来并不像真实的水。如果只看静态效果而不是动画时候会尤为明显, 因为你无法预估真正的流向应该是什么样的。这主要是因为波浪和波纹的对齐方式是错误的。它们应该沿着流向拉长,而不是垂直于流向。

(用黑色albedo扭曲各向同性图案)

扭曲效果适合于湍急或缓慢的水流。对于显示出清晰波纹图案的趋于平静的流体效果不佳,因为这种波纹应该指向明确的方向。它们是各向异性的。下面包含此类波纹的替代水纹理。它的制作方法与其他纹理相同,但图案不同,并且导数相对于高度数据的缩放比例为0.025。

(涟漪用的导数高度贴图)

导入纹理,确保它不在sRGB模式下,并将其用于扭曲效果。

(扭曲的各向异性图案)

即使没有动画,现在也有清晰的视觉方向。但是,图案与流没有对齐,因此隐含方向也不正确。如果要可视化适当的波纹,则需要使用其他方法。

1.2 方向流体Shader

在本教程中,我们将创建一个不同的流着色器。与其让纹理变形,不如让纹理与流对齐。复制DistortionFlow着色器并将其重命名为DirectionalFlow。除了不使用jump参数,我们将所有参数保持不变,因此将jump删除。另外,我们不会干扰到albedo纹理,因此可以通过主纹理提供导数高度数据。而且,我们不需要噪声来抵消阶段混合,因此我们只对流程图的RG通道感兴趣。

首先,将surf函数减少为仅对导数高度数据进行采样,对albedo使用Height平方并设置法向矢量。

使用该Shader创建一个材质,并使用和扭曲材质一样的设置,把图案改为ripple,并且tiling 设置为1。将其应用于四边形时,我们最终只会得到波纹图案。图案以与沿V轴的对齐的流相对应。默认值是向上流动的,由于图案在相反的方向上也是对称的,所以看起来一致。

(方向流动 材质)

2 与流体保持一致

现在我们有了各向异性版本了,但还需要找到一种方法将其与流向对齐。我们将首先在固定且受控的方向上进行尝试,一旦可行,便继续使用流体贴图。

2.1 方向流体的UV

使纹理与方向对齐是变换UV坐标的问题。这是对流体模拟的基石,因此我们将添在Flow文件中添加一个函数来支持它。将其命名为DirectionalFlowUV。它需要原始的UV坐标和Flow向量作为参数。还要为其提供Tiling和时间参数,类似于FlowUVW函数。由于它不需要根据时间产生变形效果,因此不需要阶段数据或时间混合权重。

我们首先简单地向上滚动,通过从V坐标中减去时间,沿正V方向移动图案。然后应用平铺。

在我们的着色器中使用此函数可获得最终流体的UV坐标。我们将为其提供float(0,1)作为流向量- [0,1]代表默认方向-切片属性,以及由速度调制的时间。然后,我们使用结果对模式进行采样。

结果与之前相同,但是有所移动。

(滑动波纹模式)

2.2 纹理旋转

要旋转UV坐标,我们需要一个2D旋转矩阵,如“渲染1,矩阵”教程中所述。如果流向量 [x, y]具有单位长度,则它表示单位圆上的一个点。因为[0,1]对应于无旋转,所以X坐标表示某个旋转角度θ(theta)的正弦,而Y坐标表示相同角度的余弦。另外,流量矢量[1,0]表示向右的U方向的流量。因此,对于顺时针旋转,流矢量可以解释为 [sinθ,cosθ]。

“渲染1,矩阵”教程将2D旋转矩阵定义为

,但它表示逆时针旋转。当我们需要顺时针旋转时,我们必须翻转sinθ的符号,这得到我们最终的旋转矩阵

因为我们的流体贴图不包含单位长度的向量,所以我们必须首先对其进行归一化。然后通过float2x2构造函数使用该方向向量构造矩阵。使用mul函数将该矩阵与原始UV坐标相乘。完成之后,应应用时间偏移和平铺。

让我们通过使用流体矢量[1,1]来测试这是否有效。这将导致图案顺时针旋转45°。

(逆时针旋转45°)

相反,我们得到了逆时针旋转。那是因为我们不是旋转图案本身,而是旋转UV坐标。为了获得正确的结果,我们必须沿相反的方向旋转它们,就像我们必须减去沿正向滚动的时间一样。因此,我们必须使用逆时针旋转矩阵。

(顺时针旋转 45°)

为了确保现在将所有流向量都转换成正确的旋转,我们使用[sintime,costime]基于时间来旋转。将材质的速度设置为零,以抵消其他的运动干扰,而让运动是由旋转唯一引起的,否则很难解释运动。

(顺时针旋转)

旋转效果正在按其应有的方式工作。动画还显示旋转位于四边形的左下角,这对应于UV空间的原点。尽管我们可以抵消旋转角度,使它以另一个点为中心,但这不是必需的。

2.3 旋转导数

尽管图案旋转正确,但法线向量还是有问题。这可能不会那么明显,但如果你仔细追究的话,它就会变得非常刺眼。使用导数让使材料着色很容易可视化。

由于各向异性模式,在零旋转时,我们通常看到绿色,很少看到红色。蓝色可以忽略,因为这是高度。

(旋转为0时候的导数)

旋转90°时我们看到什么颜色?

(90度旋转时导数不正确)

我们仍然看到相同的颜色。如果只是颜色数据,这没啥问题。但是这些是导数,代表表面曲率。当曲面旋转时,其曲率也应旋转,但这没有发生。这意味着灯光会受位置变化的影响,但不受旋转的影响。

为了保持灯光正确,我们必须旋转法线向量,这与旋转导数相同。由于DirectionalFlowUV负责旋转,因此有意义的是它还为我们提供了用于向量旋转的矩阵。通过向其添加输出参数来实现。在这种情况下,我们确实需要适当的顺时针旋转矩阵。

为此新输出提供一个变量,然后使用它来旋转我们稍后采样的导数,并进行另一个矩阵乘法。

(在90°旋转时校正了导数)

现在导数也旋转,颜色也随之改变。在90°旋转时,红色和绿色已互换。现在我们可以恢复原始颜色。

(正确旋转法向矢量)

2.4 采样流体

下一步是使用流体贴图控制旋转。对贴图采样,并将其数据提供给DirectionalFlowUV。

但是,由于我们对流向量进行归一化,因此会丢失速度信息。幸运的是,我们可以将速度存储在流程图的B通道中,因此也可以传递给DirectionalFlowUV。调整并重命名其参数,然后在添加之前以速度调整时间。

检索速度数据并将其传递给函数。但是在此之前,我们还使用“Flow Strength”着色器属性对其进行调制。变形着色器使用此属性来控制变形量,但它也会影响动画速度。尽管我们实际上不需要在方向着色器中执行此操作,但它使配置两个着色器完全相同的速度可以直接套用。并且 比较效果时很方便。

(采样流)

不幸的是,像扭曲着色器一样,我们得到了严重扭曲的无法使用的结果。独立旋转每个片段则会撕裂图案。当我们使用统一方向时,这不是问题。但对于各异向时,我不得不另寻解决方案。

3 瓦片化流体

扭曲方法存在一个暂时性的问题,因为我们被迫在某个时候重置扭曲,以保持模式不变。通过跨时间在两个不同阶段之间进行融合来隐藏了这一点而已。定向方法也存在此问题,但是性质不同。随着时间的推移,图案会逐渐破裂,但它已在0的时候被销毁了,没有任何动画。因此,重置时间将无济于事。

(扭曲,无任何运动,速度0)

取而代之的是在方向上存在差异的不连续性。这是一个空间问题,而不是时间问题。解决方法是再次通过融合隐藏问题。但是现在我们必须融合在空间中,而不是时间。而且我们正在处理2D表面,而不是一维时间,因此它将更加复杂。

我们要做的是尝试在均匀流动的完美结果与每个片段使用不同流动方向的理想结果之间找到一个折衷。折衷方案是将表面划分为多个区域。我们将仅使用正方形瓦片的网格。每个图块均具有均匀的流,因此不会遭受任何扭曲。然后,我们将每个图块与其邻居混合在一起,以隐藏它们之间的不连续性。Frans van Hoesel于2010年首次将该方法称为平铺方向流算法(Tiled Directional Flow)。我们将为其创建一个变体。

3.1 流体网格

要将表面拆分为图块,我们需要确定网格分辨率。我们将通过着色器属性(默认值为10)使它可配置。

(网格分辨率设置为10)

通过将用于采样贴图的UV乘以网格分辨率,然后丢弃小数部分,可以将流体贴图切成图块。这使我们的瓦片具有固定的UV坐标,从0到网格分辨率。要将其转换回0到1的范围,请除以平铺坐标除以网格分辨率。

(每个网格单元的一个流向)

3.2 融合单元

现在,我们具有明显可区分的网格单元,每个网格单元包含一个不扭曲的图案。下一步是将它们进行混合。这需要我们为每个片段采样多个单元。因此,让我们移动代码以将导数和高度数据计算到新的FlowCell函数中。最初,所需的只是原始UV坐标和缩放时间。

可以通过在对UV坐标求底以找到固定流之前添加偏移来对其他单元进行采样。为此添加一个参数到FlowCell。

首先让我们尝试在U维度上偏移一个单位。这意味着我们最终将向右采样一个单元,在视觉上将流数据向左移动一级。

(单元格向右偏移一步)

为了水平混合单元格,我们必须对每个图块同时采样原始像元和偏移像元。我们将原始数据指定为A,将偏移数据指定为B。将它们平均化,然后每个权重赋予0.5并将其求和。

(平均单元格)

现在,每个图块都包含相同数量的A和B。接下来,我们必须沿U维从A过渡到B。我们可以通过在A和B之间进行线性插值来实现。缩放后的U坐标的小数部分是可以用来插值权重的值t。让我们通过将其用作albedo来对其进行可视化。

(基础插值)

A单元格在每个图块的左侧以最大强度开始,其中 t为零。它应该在什么时候消失 t到达右侧的1。所以A的权重是 t-1。B在另一边,所以它的权重很简单 t。

(水平内插单元)

3.3 重叠单元

尽管单元之间的插值应该消除了水平不连续性,但我们仍然可以看到很明显的网格线。这些线是由用于采样流图的UV坐标的突然跳跃引起的失真。突然大的UV增量会触发GPU沿着网格线选择不同的mipmap级别,从而破坏流数据。尽管我们可以通过消除mipmap来消除这些伪像,但这不是可取的。能不能有其他方式将它们隐藏起来呢。

可以通过确保单元线所在的单元格权重在其边缘为零来隐藏线。但是权重函数t重置每个图块,因此边缘上的锯齿波均为0和1。因此,尽管一侧总是很好,但另一侧却显示了失真。

(网格线处的锯齿波均为0和1)

为了解决这个问题,我们必须重叠单元。这样,我们就可以在它们之间交替使用,并使用其中一个隐藏另一个。首先,将第二个单元的偏移减半。在FlowCell中执行此操作最合适了,因此我们可以继续使用整数作为offset参数。着色器编译器会消除多余的计算。

(重叠单元格)

现在,水平单元重叠,发生频率是我们实际使用的图块的两倍。接下来,我们必须再次正确地混合单元。这可以通过用 | 2t-1 |替换 实现,将其变为在瓦片的两侧为零而在中间为1的三角波。

(三角波在网格线处始终具有相同的值,即0或1)

更改的结果是,每个图块的两边的A权重现在为零。它在中途处于全强度。B的另一种情况是,每个图块的中间权重为零。而且由于我们现在仅将B偏移一半,因此这正是其失真线显示的位置。

(单元格水平混合而没有失真)

既然我们可以融合而没有失真,那么我们也可以垂直进行。添加单元C和D,它们在V维度上相对于A和B都偏移了一步。

现在必须将A和B的权重在V维度上乘以1-t, C和D乘以t。每个维度都有自己的 t值,可以通过将其更改为float2并从两个UV坐标派生来实现。

(2个维度上进行混合)

3.4 采样网格中心

目前,我们正在每个瓦片的左下角采样流体。但这与我们混合单元的方式不一致。结果导致流数据之间的混合未对齐,这使得网格比应有的更加明显。相反,我们应该在每个单元格的权重为1的中心处对流进行采样。对于单元格A,它位于每个图块的中间,因此需要将其采样点移到那里。对于B至少在V维度上也是如此。由于B已在U维度上偏移了一半的图块,因此不需要水平移动。C和D在V维度上很好,但是C需要水平移动。

通常,在没有偏移的情况下,我们必须平移一半的图块,反之亦然。我们可以方便地在FlowCell中执行此操作,方法是将未缩放的偏移量减去1并将其减半。然后将其添加到flooring之后的分割区域中的UV坐标中。

(中心流体采样)

现在,我们可以正确使用流数据,但是准确度取决于网格分辨率。分辨率越高,流动曲线越平滑。但是也不能将分辨率设置得太高,因为会出现波纹图案。

(Tiling 1,网格分辨率30)

增加平铺可以使分辨率提高,但也可以减小纹波。你需要找到一种最适合每种情况的平衡。例如,对于本教程中的图像,将5的Tiling与30的网格分辨率结合起来效果很好。这样就可以看到水流,而波纹也不会很小。

(Tiling 5,网格分辨率分别为10,30)

3.5 缩放波浪

就像我们对变形效果所做的一样,我们还使用恒定因子并通过流动强度来调整导数和高度数据的强度。

(按流速缩放)

由于我们用的是空间方法,现在还可以根据流速来缩放图案尺寸。快速流动的河流有许多小波动,而较慢的区域则有较少的大波动。我们可以通过考虑平铺中的流速来支持这一点。

当流速非常低时(由于我们使用0.1的流动强度),这种情况会退化,因为图案会变得特别大。每个单元中只能容纳一个非常小的波纹图案区域。

我们仍然可以适度缩放模式。我们可以通过为恒定平铺和调制平铺都设置一个属性,以与缩放高度相同的方式执行此操作。我将恒定平铺设置为3,将调制平铺设置为50。调制平铺必须设高以补偿低流速。

(Constant tiling 3 ,modulated tiling 50)

最终平铺等于流速乘以modulated tiling再加上原始的Constant tiling。

(恒定和调制平铺)

4 隐藏失真

尽管我们的定向流体着色器现在已经完成了功能,但不幸的是仍然存在一些失真。尽管它们并不是很明显,但仍需要关注。

最明显的失真是可见的平铺,其中流向变化相当快。这对于我们的流体贴图而言非常明显,因为它有很多弯曲。这可以通过增加网格分辨率来解决,但也需要增加平铺。

(增加网格分辨率和平铺)

4.1 几乎均匀的水流

真正有问题的失真出现在流体变化不大的区域。如果流确实是均匀的,则无法隐藏图案的平铺。如果想复现这个效果,只要将平铺的UV坐标强制为零,那么到处就都会使用相同的流数据了。

(均匀的流体)

可见的平铺图可以通过使用较大的波纹图案来去除,但这有其局限性。真正防止这种情况发生的唯一方法是确保它至少发生一些变化,比如在生成流体贴图时增加噪音。这是一种很好的方法,因为液体很少能完全均匀地流动。通常存在以某种方式影响流量的隐藏或淹没因素。因此,让我们考虑一个大致均匀的流动,例如缓慢弯曲的电流一样。我们可以通过将流量采样临时缩放0.1来看到这种情况。

可以发现与动画过程中的流量相匹配的脉冲模式,但是咋一看很难注意到。将速度设置为零时,会出现此问题的更明显体现。例如可以看到突然出现条纹,这是由于波纹图案的几乎相同的区域重复出现,并略有偏移,旋转和缩放所致。

(缩放流体贴图)

流体贴图的压缩和纹理过滤可以在某种程度上帮助掩盖这些失真。当使用未压缩的流体贴图时,失真会发生变化,甚至变得更加明显。

(未压缩的流体贴图)

这些问题是由快速重复图案引起的。虽然降低网格分辨率有助于降低此效果,但也会使流动不那么顺畅。幸运的是,当对每个单元采样时,我们可以通过抖动UV坐标来混淆重复。只需添加像元偏移即可。

(加上图案偏移)

由于这会增加单元格图案之间的差异,因此还会添加更明显的动画。这使波纹更生动。

4.2 观察网格

还有一种失真,是由单元格之间的混合引起的。如果方向或速度差异足够大,则平铺可能会变得很明显。例如,在我们放大流体贴图的同时,将网格分辨率设置为3。

(网格分辨率为3)

现在,可以清楚地看到较暗或较亮的图块。这是由于每个瓦片的流速不同所致。但这不是最有问题的部分。我们可以使用黑色消除这种情况。

(黑色)

当你注意镜面反射时,仍然可以看到网格。这是因为混合单元的区域比单个单元占主导的区域要平坦。结果,镜面反射以网格图案变化。由于此模式是静态的,因此在激活波纹时会更加突出。

(看高光部分)

4.3 混合网格

没有简单的方法可以消除镜面反射失真,就像我们无法完全消除扭曲效果的阶段混合失真一样,只是用噪声对其进行模糊处理。在这种情况下,用噪声干扰网格不会使它变得不那么明显。而且,平滑混合功能不会消除它们,实际上,任何更改都会使它们更加明显。

消除失真的唯一方法是摆脱均匀区域和混合区域之间的过渡,但这是不可能的。接下来的最好的办法就是涂抹差异。

我们可以做的是对整个网格进行两次采样。如果我们将第二个网格偏移四分之一格,则其最清晰的区域对应于另一个网格的最模糊区域,反之亦然。如果再对这两个网格进行平均,那么最终将得到更加均匀的混合。

将采样和合并四个单元合并的代码移动到新的FlowGrid函数。

现在,我们将对两个网格进行采样,就像我们为扭曲效果采样了两个阶段一样。再次,我们可以使用布尔参数来表示我们要变体A还是变体B。然后对两者进行采样并取平均值。

如果是变体B,我们必须改变权重函数。缩放后添加四分之一,然后取小数部分。

我们还必须告诉FlowCell需要哪个变体。替代网格必须偏移四分之一,并且样本偏移必须在另一个方向上偏移以进行补偿。

(混淆网格)

这不能完全消除问题,但是会使其不那么明显。

4.4 可选的混合项

组合两个网格比仅使用一个网格要耗费更多的工作。如果网格不明显(例如,因为镜面反射不多),那么你可能只需要一个网格。因此,让我们将双网格设为可选。这也使得比较这两种方法更加容易。向着色器添加一个切换开关,来切换功能。这是具有Toggle属性的整数属性。此属性需要关键字作为参数,我们将使用_DUAL_GRID。

着色器不使用属性的整数部分,仅关键字很重要。通过检查器检查属性时,将定义该关键字,否则未定义。

将#pragma shader_feature _DUAL_GRID语句添加到着色器中,在#pragma target 3.0的正下方。这指示Unity编译我们的着色器的两个变体。一启用和一未启用关键字。使用哪一种取决于材料是否已检查属性。

现在,仅在定义关键字时才包括对第二个网格进行采样并求均值的代码行。可以将它包含在预处理程序的#if和#endif指令之间。#if后跟define(_DUAL_GRID),用于检查是否定义了关键字。只有这样才能包含代码。这是编译过程的预处理步骤。一个着色器变体中包含该行代码,另一个则没有。

(切换双网格模式)

最后,删除流体贴图的临时缩放比例。

当使用平铺缩放时,双网格还为我们提供了更多的摆动空间。

(调整和给流体上色)

下一节,介绍波浪。

本文翻译自 Jasper Flick的系列教程

原文地址:

https://catlikecoding.com/unity/tutorials

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 壹种念头 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档