Unity 水、流体、波纹基础系列(一)——纹理变形(Texture Distortion )

2020-08-28 10:04:05 浏览数 (1)

本文重点: 用一张流图纹理来调整UV坐标 创建一个无缝的动画循环 控制流体产生 使用导数纹理展示凹凸

1 UV动画

如果液体不动时,在视觉上是无法与固体区分开的。你看的到底是水,果冻还是玻璃杯呢?水池是结冰的吗?但可以肯定的是,如果干扰它并观察它是否会变形,以及变形多少就可以区分。仅从创建上看起来像流体的材质是远远不够的,实际上它必须要能动起来。否则,它就是看起来像是水的玻璃雕塑或已经结冰的水。当然,这对于一张照片来说已经足够了,但对于电影或游戏来说远远足够。

在大多数情况下,我们只希望表面由水,泥,熔岩或某种看起来像液体的神奇效果制成。它不需要是交互式的,只是在随意观察时看起来很像。因此,我们不需要进行复杂的水物理模拟。需要做的只是在常规材质中添加一些动画。这可以通过对用于纹理化的UV坐标进行动画处理来完成。

Valve的Alex Vlachos首先在SIGGRAPH2010演示文稿《 Portal 2中的水流》中公开详细描述了本教程中使用的技术。

1.1 滑动表面着色器

对于本教程而言,你可以重新建一个新项目,设置为使用线性色彩空间渲染。如果你使用的是Unity 2018,请选择默认的3D管道,而不是轻量级或HD。然后创建一个新的标准表面着色器。我们要通过扭曲纹理贴图来模拟流体的表面,因此将其命名为DistortionFlow。下面是新的着色器,其中删除了所有注释和不需要的部分。

为了易于查看UV坐标如何变形,可以使用如下测试纹理。

(UV测试纹理)

创建我们的着色器的材质,并将测试纹理作为其albedo贴图。将其tiling设置为4,以便我们可以看到纹理是如何重复的。然后使用此材质将四边形添加到场景中。为了获得最佳观看效果,请将其绕其X轴旋转90°,以使其在XZ平面中平放。这样可以轻松地从任何角度查看它。

(Distortion Flow材质在四边形上)

1.2 让UV流动

流体UV坐标的代码是通用的,因此我们将其放在单独的Flow.cginc包含文件中。它需要包含的只是一个具有UV和时间参数的FlowUV函数。它应该返回新的流体UV坐标。我们从最简单的位移开始,这只是将时间添加到两个坐标。

将此文件包含在我们的着色器中,并使用主要的纹理坐标和当前时间调用FlowUV,Unity通过_Time.y使其可用。然后使用新的UV坐标来采样我们的纹理。

(对角线滑动的UV)

当我们将两个坐标增加相同的数量时,纹理将沿对角线滑动。加上了时间之后,所以它从右上方滑动到左下方。并且由于我们为纹理使用默认的环绕模式,因此动画每秒循环一次。

仅当时间值增加时,动画才可见。当编辑器处于播放模式时就是这种情况,但是你也可以通过“场景”窗口工具栏启用“Animated Materials”来在编辑模式下启用时间进度。

(启用Animated Materials)

实际上,每次编辑者重新绘制场景时,素材使用的时间值都会增加。因此,当禁用“Animated Materials”时,每次你编辑某些内容时,纹理滑动都会有产生一点点。动画材质只会强制编辑器始终重绘场景。因此,仅在需要时才打开它吧。

1.3 流动方向

你可以使用速度矢量来控制流动的方向和速度,而不必总是沿相同的方向流动。因此可以将此矢量作为属性添加到材质。但是,我们仍然仅限于对整个材质使用相同的矢量,但这看起来像是的硬的物体表面在滑动。为了使某种东西看起来像流动的液体,除了一般移动之外,它还必须随时间局部变化。

通过添加另一个速度矢量来消除静态外观,使用该速度矢量第二次对纹理进行采样,然后将两个采样组合在一起。当使用两个略有不同的矢量时,我们最终得到一个变形纹理。但是,我们仍然仅限于以相同方式流动整个表面。对于开阔水域或直流而言,这通常就足够了,但在更复杂的情况下则不足。

为了支持更多有趣的流体效果,我们必须以某种方式改变整个材质表面的流体向量。最简单的方法是通过Flow 贴图。这是包含2D向量的纹理。这是一种纹理,在R通道中具有向量的U分量,在G通道中具有向量的V分量。它不需要很大,因为我们并不需要展示急剧的突然变化,依靠双线性滤波来来保持平滑。

(Flow 贴图)

该纹理是使用卷曲噪声创建的,在“噪波导数”教程(尚未翻译)中对此进行了说明,但是创建纹理的细节并不重要。它包含多个顺时针和逆时针旋转流,没有任何源或汇。确保将其导入为不是sRGB的常规2D纹理,因为它不包含颜色数据。

将流体 贴图的属性添加到我们的材质中。它不需要单独的UV平铺和偏移,因此为其指定NoScaleOffset属性。默认值为没有flow,默认对应于黑色纹理。

(带有流体 贴图的材质)

为流体贴图添加一个变量,并对其进行采样以获得流向量。然后通过将其用作于albedo进行临时可视化。

(平铺流体向量)

纹理是线性数据,因此在场景中显得更亮。很好,因为无论如何我们都不应该将其用作颜色。由于表面着色器的主要UV坐标使用了主要纹理的平铺和偏移,因此我们的流图也会平铺。我们不需要映射流体贴图,因此将材质的Tileing设置回1。

(没有Tileing的流体向量)

1.4 定向滑动

现在我们有了流体向量,我们可以在FlowUV函数中添加对它们的支持。为它们添加一个参数,然后将它们乘以时间,再减去原始UV。之所以要减去,是因为这会让效果向矢量方向流动。

将流体向量传递给函数,但在执行此操作之前,请确保该向量有效。与法线贴图一样,向量可以指向任何方向,因此可以包含负分量。因此,矢量的编码方式与法线贴图相同。我们必须手动对其进行解码。同样,恢复原始的albedo。

(太多的扭曲了)

这导致了十分扭曲的纹理效果。发生这种情况是因为纹理在多个方向上移动,随着时间的流逝越来越多地拉伸和挤压它。为了防止它变得混乱,我们必须在某个时候重置动画。最简单的方法是仅使用动画时间的一小部分。因此,它通常从0上升到1,然后重置为0,形成锯齿状。

(锯齿样的进度)

由于这是特定于流体动画而不是通常的时间,因此请在FlowUV中创建锯齿进度。

(每秒重置)

现在我们可以看到纹理确实在不同的方向上以不同的速度变形了。除了突然的重置,最明显的是纹理随着变形的增加而迅速变得块状。这是由于流体贴图压缩引起的。默认压缩设置使用DXT1格式,这是块状性的来源。这些伪影在使用有机纹理时通常并不明显,但在使清晰的图案(例如我们的测试纹理)变形时会刺眼。因此,本教程中的所有屏幕截图和动画都使用了未压缩的流体贴图。

(没有压缩)

为什么不使用更高分辨率的图? 尽管是可以的,但流体贴图通常会覆盖较大的区域,因此最终导致有效分辨率很低。只要不使用极端变形,就没有问题。本教程中显示的变形非常强烈,以使其在视觉上更加明显。

2 无缝循环

此时,我们可以为非均匀流体设置动画,但它会每秒重置一次。为了使其循环不间断,我们必须以某种方式使UV恢复到变形之前的原始值。时间只会往前,所以我们无法倒退回去。尝试执行该操作将导致流体来回移动,而不是方向一致。我们必须找到另一种方式。

2.1 混合权重

虽然无法避免重置变形的进程,但是我们可以尝试隐藏它。我们可以做的就是在接近最大扭曲时将纹理淡化为黑色。如果我们也从黑色开始并且在开始时在纹理中淡入淡出,那么当整个表面为黑色时再马上重置。尽管这很明显,但至少没有突然的视觉不连续。

为了完成淡入淡出,让我们在FlowUV函数的输出中添加一个混合权重,并将其重命名为FlowUVW。权重放在第三个分量中,到目前为止,有效分量一直是1,所以让我们开始吧。

我们可以通过将其乘以着色器现在可用的权重来使纹理褪色。

2.2 跷跷板

现在我们必须创建一个权重函数w(p),其中w(0)= w(1)= 0。在中途它应该达到峰值,所以 w(1/2)= 1。满足这些条件的最简单函数是三角波 w(p)= 1- | 1-2p |。用它来减轻我们的权重。

(带有三角波函数的锯齿)

(加入三角波函数之后的效果)

为什么不使用更加平滑的函数? 你也可以使用正弦波或应用smoothstep函数。但是这些功能会使着色器更加复杂,而对最终结果的影响不大。三角波就足够了。

2.3 时间偏移

从技术上讲,我们已消除了视觉上的不连续性,但引入了黑色脉冲效应。脉冲非常明显,因为每一次循环都在发生。如果我们可以随着时间的流逝而扩展它,可能会让它变得不太明显。通过在整个表面上偏移时间来实现此目的。一些低频Perlin噪声非常适合于此。无需添加其他纹理,而是将噪声打包在流体贴图中。这是与以前相同的流体贴图,但现在A通道中有噪音。噪声与流向量无关。

(A通道带有噪声的流体贴图)

为了表明我们期望流体贴图中有噪声,更新其标签。

采样噪声并将其添加到传递给FlowUVW之前的时间。

(带有时间偏移的效果)

为什么采样流体贴图两次? 说明一下,着色器编译器会将其优化为单个纹理样本,黑色脉冲仍然存在,但是已经变成了以机械方式在表面传播的波。 与一致的脉冲相比,混淆起来容易得多。 另外,时间偏移还使扭曲的进行变得不均匀,从而导致总体上扭曲的变化更大。

2.4 结合两个不同的扭曲

我们可以不融合为黑色,而可以融合其他元素,例如原始的未发生扭曲的纹理。但是,随后我们会看到固定的纹理淡入淡出,这将破坏流动的幻觉。我们可以通过与另一个变形的纹理融合来解决。这要求我们对纹理进行两次采样,每个采样具有不同的UVW数据。

这样,我们最终会得到两个脉冲模式A和B。当A的权重为0时,B的权重应为1,反之亦然。这样黑脉冲就被隐藏了。这是通过将B的相位偏移其周期的一半来完成的,这意味着将其时间增加0.5。这是FlowUVW的工作方式的详细信息,因此,我们只需添加一个布尔参数即可指示我们是否要为A或B变体使用UVW。

(A和B的权重相加为1)

现在,我们必须两次调用FlowUVW,一次使用false,最后一次使用true。然后对纹理进行两次采样,将它们的权重相乘,然后相加,得出最终的albedo。

(融合2个阶段)

黑色的脉冲波不再可见。波浪仍然存在,但是现在形成了两个阶段之间的过渡,这已经不那么明显了。

在两个模式之间偏移一半周期的混合副作用是动画的持续时间减少了一半。现在,它每秒循环两次。但是我们不必两次使用相同的模式。我们可以将B的UV坐标偏移半个单位。这将使图案不同(同时使用相同的纹理),而不会引入任何方向的偏差。

(A和B分别取不同的UV)

因为我们使用常规测试图案,所以A和B的白色网格线重叠。但是它们的方块颜色不同。结果,最终动画在两种颜色配置之间交替,并再次花费一秒钟重复。

2.5 UV跳跃

除了始终将A和B的UV偏移半个单位外,还可以按阶段偏移UV。这将导致动画随时间变化,因此它需要更长的时间才能循环回到完全相同的状态。

我们可以简单地基于时间滑动UV坐标,但这将导致整个动画滑动,从而引入方向偏差。我们可以通过在每个阶段保持UV偏移恒定,然后在各个阶段之间跳转到新的偏移来避免视觉滑动。换句话说,每次权重为零时,我们都会使UV跳变。这是通过在UV上加上一些跳跃偏移量乘以时间的整数部分来完成的。调整FlowUVW以支持此功能,并使用新参数指定跳转向量。

在我们的着色器中添加两个参数以控制跳转。可以使用两个浮点数代替单个向量,这样我们就可以使用范围滑块。因为我们在两个偏移一半的图案之间进行混合,所以我们的动画已经包含了每个阶段的UV偏移序列0→1/2 。在此之上添加跳转偏移量。这意味着如果我们跳到一半,进度将在两个阶段变成 0→1/2→1/2→0,这不是我们想要的。我们最多应跳四分之一,这将在四个阶段中产生0→1/2→1/4→3/4→1/2→0→3/4→1/4 。负偏移最多也可以达到四分之一。这将产生序列 0→1/2→3/4→1/4→1/2→0→1/4→3/4。

将所需的float变量添加到我们的着色器,使用它们构造跳转向量,并将其传递给FlowUVW。

(具有最大jump的材质)

在最大跳跃的情况下,在重复之前,我们将经历八个UV偏移的序列。当我们每个阶段经历两个偏移并且每个阶段都是一秒时长,所以我们的动画现在每四秒钟循环一次。

2.6 分析跳跃

为了更好地了解UV跳跃的工作原理,可以将流体矢量设置为零,以便集中于偏移量。首先,考虑动画没有任何跳跃,只是原始的交替模式。

(Jump 0 持续1秒)

你会看到每个正方形在两种颜色之间交替。你还可以看到,我们在相同的纹理偏移量之间进行了交替,但这并不特别明显,也没有方向偏差。接下来,看一下在两个方向上跳动最大的动画。

(Jump 0.25 持续4秒)

结果看起来有所不同,因为跳跃四分之一会导致测试纹理的网格线移动,在正方形和十字形之间交替。白线仍然没有显示方向偏差,但是彩色正方形现在可以了。模式沿对角线移动,但不是立即可见。向前走半步,然后向后走四分之一步,重复一次。如果我们使用-0.25的最小跳跃,那么它将向前走半步,然后向前走四分之一步,重复一次。为了使方向偏差更明显,请使用不对称的跳转,例如0.2。

(Jump 0.2 持续2秒)

这时,白色网格线也会移动。但是,由于我们仍在使用相当接近对称的大跳跃,因此可以将移动解释为向多个方向移动,具体取决于你对图像的聚焦方式。如果你改了变焦点的话,则很容易就无法确定方向。

因为我们使用的是0.2的跳跃值,所以动画在五个阶段后重复播放,因此持续了五秒钟。但是,由于我们在两个偏移阶段之间进行了混合,因此每个阶段的中间都有一个潜在的交叉点。如果动画将在奇数个相位后循环,则实际上在阶段相交一半时会循环两次。因此,持续时间反而变为2.5秒。

其实不必将U和V跳跃相同的数量。除了改变方向偏差的性质外,每个维度使用不同的跳变值还会影响环路持续时间。例如,假设U跳为0.25,V跳为0.1。U每四个周期循环一次,而V每十个循环一次。因此,在四个循环之后,U循环了,但是V尚未循环,因此动画也没有完成循环。只有当U和V在同一阶段的末尾都完成一个循环时,我们才到达动画的末尾。当对跳使用有理数时,循环持续时间等于其分母的最小公倍数。在0.25和0.1的情况下,分别是4和10,最小公倍数是20。

没有明显的方法可以选择跳跃向量,因此循环时间长。例如,如果我们使用0.25和0.2代替0.25和0.1,那么持续时间会更长或更短吗?由于4和5的最小公倍数也是20,因此持续时间是相同的。另外,虽然你可能会得出理论上需要很长时间甚至是永远循环的值,但大多数值实际上没有用。我们无法感知到太小的变化,再加上数值精度的局限性,这可能会导致理论上好的跳跃值在偶然的观察下不会改变或比预期的更好。

我认为良好的跳变值(除零外)应介于0.2到0.25之间(正数或负数)。我得出6/25 = 0.24和5/24≈0.2083333作为适合标准的简单对。第一个值在25个阶段后完成六个跳跃周期,而第二个值在24个阶段后完成五个周期。整个理论循环需要600个阶段,即每秒一阶段的速度需要十分钟。

在本教程的其余部分中,我将跳转值保留为零,以便使循环动画保持简短。

3 动画调整

现在我们有了基本的流体动画,让我们为其添加更多配置选项,以便我们对其效果进行微调。

3.1 平铺

首先,让我们可以平铺扭曲的纹理。不能只依赖表面着色器的主平铺和偏移,因为这也会影响流体贴图。相反,我们需要为纹理提供单独的切片属性。通常只有扭曲正方形纹理才有意义,因此我们只需要一个平铺值。

为使流动与平铺无关而保持相同,我们必须在流动后但在添加B相偏移之前将其应用于UV。因此需要在FlowUVW中完成,这意味着我们的函数需要一个平铺参数。

也向我们的着色器添加一个平铺属性,默认值为1。

然后添加所需的变量并将其传递给FlowUVW。

(Tiling设置为2 持续时间仍然为1 )

当平铺设置为2时,动画的流动速度似乎是以前的两倍。但这仅仅是因为纹理已缩放。不跳过UV时,动画仍然需要一秒钟循环播放。

3.2 动画速度

动画速度可以通过缩放时间直接控制。这会影响整个动画,并影响其持续时间。添加一个速度着色器属性以支持此操作。

只需将_Time.y乘以相应的变量即可。之后应添加噪声值,因此时间偏移不会受到影响。

(速度设置为0.5,时间变为2秒)

3.3 流动强度

流速由流体贴图决定。我们可以通过调整动画速度来加快或降低速度,但这也会影响阶段长度和动画持续时间。改变视觉上的流速效果的另一种方法是缩放流体向量。通过调整流体强度,我们可以在不影响时间的情况下加快,减慢甚至逆转它。这也改变了扭曲量。添加“Flow Strength”着色器属性试试。

在使用之前,只需将流向量乘以相应的变量即可。

(Flow strength 设置为0.25,持续2秒)

3.4 流偏移

另一个可以调整的是控制动画的开始位置。到现在为止,我们总是在每个阶段的开始时从零开始进行扭曲,一直到最大。当阶段的权重在中点达到1,扭曲的强度为一般时,图案最清晰。因此,我们通常会看到半扭曲的纹理。这种配置通常很好,但并非总是如此。例如,在“传送门2”中,漂浮的碎片纹理大部分处于未扭曲状态。这是通过UV坐标扭曲时把流体偏移-0.5来完成的。

通过向FlowUVW添加flowOffset参数,我们也可以支持这一效果。仅与流向量相乘,然后将其添加到进度中。

接下来,添加一个属性以控制着色器的流偏移。它的实际值为0和-0.5,但是你也可以尝试其他值。

将相应的变量传递给FlowUVW。

(Flow offset 设置为0.5)

流量偏移为-0.5时,每个相位的峰值处都没有扭曲。但是由于时间偏移,总体结果仍会扭曲。

4 纹理化

我们的扭曲流体着色器现在可以正常使用了。让我们看看它与到目前为止使用的测试纹理不同的外观。

4.1 抽象水纹

扭曲效果最常见的用途是模拟水面。但是因为变形可以在任何方向上进行,所以不建议使用特定流动方向性的纹理。不建议使用方向行就不可能做出正确的波浪,但是我们不需要真实。当纹理变形和融合时,它只要看起来像水就好。例如,这是一个简单的噪声纹理,它结合了一个八度的低频Perlin和Voronoi噪声。它是水的抽象的灰度表示,波浪的底部是深色,波浪的顶部是浅色。

(水纹纹理)

使用此纹理作为我们材质的albedo贴图。除此之外,我没有使用跳跃。平铺为3,速度为0.5,流动强度为0.1以及无流量偏移。

(流动的水)

噪波纹理本身看起来并不像水,但扭曲和动画效果让它看起来有点像水了。你还可以通过将流动强度临时设置为零来检查其外观是否不不扭曲。这将代表静止的水,并且看起来应该至少可以接受。

(模拟平静的水面)

4.2 法线贴图

albedo贴图仅是预览,因为流动的水主要由其表面垂直变化的方式定义,这会改变它与光的交互方式。为此,我们需要一个法线贴图。这是通过将albedo纹理解释为高度图而创建的,但高度按0.1缩放,因此效果不太强。

(法线贴图)

为法线贴图添加一个着色器属性。

采样A和B的法线贴图,应用它们的权重,并将它们的归一化总和用作最终表面法线。

将法线贴图添加到我们的材质中。还可以将其平滑度增加到大约0.7,然后更改光线,以便获得大量的镜面反射。我们将视图保持不变,但是将定向光旋转了180°至(50,150,0)。同时将albedo设置为黑色,因此我们只能看到法线动画的效果。

(动态水流)

扭曲且生动的法线贴图产生了令人信服的流动水幻象。但是当流动强度为零时如何保持呢?

(静态水)

(最大的jump 速度设置为1)

4.3 导数贴图

尽管生成的法线看起来不错,但对法线进行平均并没有多大意义。正确的方法是将法线向量转换为高度导数,将它们相加,然后转换回法线向量。对于穿过表面传播的波来说尤其需要如此。

由于我们通常对法线贴图使用DXT5nm压缩,因此我们首先必须重建两个法线的Z分量(这需要平方根计算),然后转换为导数,合并并归一化。但是我们不需要原始的法线向量,因此我们也可以通过将导数存储在贴图中而不是用法线来跳过转换。

导数贴图的工作方式与法线贴图相同,不同之处在于它包含X和Y维度上的高度导数。但是,如果没有额外的缩放比例,导数贴图只能支持最大45°的表面角度,因为该角度的导数为1。由于通常不会使用这种陡峭的波,因此该限制是可以接受的。

这是一个与以前的法线贴图描述相同表面的导数贴图,就像法线贴图一样,X导数存储在A通道中,Y导数存储在G通道中。另外,它的B通道中还包含原始高度图。但是同样,通过将高度缩放0.1来计算导数。

(导数加高度图)

为什么不把高度直接用0.1倍存储? 高度数据以最大强度存储,以最大程度地减少精度损失。

由于纹理不是法线贴图,因此将其导入为常规2D纹理。确保指示它不是sRGB纹理。

将我们的导数加高地图替换为普通地图着色器属性。

还要替换着色器变量,采样和常规构造。我们不能再使用UnpackNormal,因此创建一个自定义的UnpackDerivativeHeight函数,该函数将正确的数据通道放入浮点向量并解码导数。

(用导数图代替法线图)

生成的表面法线看起来几乎与使用法线贴图时的外观相同,但它们的计算成本较低。由于我们现在也可以访问高度数据,因此我们也可以使用它为表面着色。这对于调试很有用,因此让我们暂时替换原始的albedo。

(使用高度作为albedo)

该表面看起来比使用albedo纹理时更亮,即使两者都包含相同的高度数据。但 有所不同,因为我们现在使用线性数据,而albedo纹理被解释为sRGB数据。为了获得相同的结果,我们必须手动将高度数据从gamma转换为线性色彩空间。我们可以通过简单地平方来近似。

(使用高度的平方)

4.4 高度缩放

使用导数而不是法向量的另一个好处是可以轻松缩放它们。导数的法线将与调整后的曲面匹配。这使得可以正确地缩放波浪的高度。让我们向着色器添加一个高度比例属性以支持此操作。

将高度比例因素分解到采样的导数加上高度数据中。

但是我们可以走得更远一些。比如,根据流速使高度比例可变。这个想法是,当流量大时,你会得到较高的波浪,而流量小时,你将会得到较低的波浪。为了控制它,添加第二个高度比例属性,用于基于流速的调制高度。另一个属性保持不变的规模。最终的高度比例可以通过组合两者来找到。

流速等于流速矢量的长度。将其乘以调制比例,然后加上恒定比例,并将其用作导数加高度的最终比例。

虽然可以完全根据流速来确定高度比例,但最好至少使用一个较小的恒定比例,这样在没有流速的地方表面不会变得平坦。例如,使用0.1的恒定比例和9的调制比例。它们不需要加1,设置取决于您希望最终法线的强度和所需的多样性。

(常数加调制后的强度)

4.5 流量速度

与其在着色器中计算流速,不如将其存储在流程图中。尽管采样过程中的滤波可以非线性地改变矢量的长度,但是只有在对两个非常不同的矢量进行插值时,这种差异才会变得很明显。只有当我们的流体贴图中方向突然改变时,情况才会如此。只要没有这些,对存储的速度向量进行采样就会产生几乎相同的结果。另外,调制高度比例时不一定要完全匹配。

现在将速度值存储在流体贴图的B通道中。

(B通道中具有速度的流体贴图)

使用采样数据而不是自己计算速度。由于速度没有方向,因此不应进行转换,这与速度矢量不同。

我们通过恢复原始albedo来结束教程。再将材质颜色更改为蓝色,具体是(78,131,169)。

(最终水纹,jump最大)

可信的水效果最重要的品质是其动画表面法线的质量。一旦这些效果很好,你就可以添加额外的效果,例如更高级的反射,透明度和折射。但是即使没有这些附加功能,该表面也将被看做为水。

下一节,介绍定向流动。

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

原文地址:

https://catlikecoding.com/unity/tutorials

0 人点赞