原文地址:https://catlikecoding.com/unity/tutorials/flow/texture-distortion/
大纲:
使用流图调整UV坐标。
创建一个无缝的动画循环。
控制流的外观。
使用导数贴图添加凹凸。
1.滚动uv
在大多数情况下,我们只希望表面由水,泥,熔岩或某种看起来像液体的神奇效果制成。它不需要是交互式的,只是在随意观察时看起来很可信。因此,我们不需要进行复杂的水物理模拟。我们需要的是在常规材料中添加一些动作。这可以通过对用于纹理化的UV坐标进行动画处理来完成。
1.1 滚动Surface Shader
对于本教程,您可以从一个新项目开始,设置为使用线性色彩空间渲染。 如果您使用的是Unity 2018,请选择默认的3D管道,而不是轻量级或HD。 然后创建一个新的标准表面着色器。 当我们要通过扭曲纹理贴图来模拟流动的表面时,将其命名为DistortionFlow。 下面是新的着色器,其中删除了所有注释和不需要的部分。
Shader "Custom/DistortionFlow" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _Glossiness ("Smoothness", Range(0,1)) = 0.5 _Metallic ("Metallic", Range(0,1)) = 0.0 } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Standard fullforwardshadows #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; half _Glossiness; half _Metallic; fixed4 _Color; void surf (Input IN, inout SurfaceOutputStandard o) { fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
为了易于查看UV坐标如何变形,可以使用此测试纹理。
创建使用我们的着色器的材质,并将测试纹理作为其反照率贴图。 将其平铺设置为4,以便我们可以看到纹理如何重复。 然后使用此材质将四边形添加到场景中。 为了获得最佳观看效果,请将其绕其X轴旋转90°,以使其在XZ平面中平放。 这样就可以轻松地从任何角度查看它。
1.2 让UV流动起来
流动UV坐标的代码是通用的,因此我们将其放在单独的Flow.cginc包含文件中。 它需要包含的只是一个具有UV和时间参数的FlowUV函数。 它应该返回新的流动UV坐标。 我们从最简单的位移开始,这只是将时间添加到两个坐标。
#if !defined(FLOW_INCLUDED) #define FLOW_INCLUDED float2 FlowUV (float2 uv, float time) { return uv + time; } #endif
将此文件包含在我们的着色器中,并使用主要纹理坐标和当前时间调用FlowUV,Unity通过_Time.y使其可用。 然后使用新的UV坐标来采样我们的纹理。
#include "Flow.cginc" sampler2D _MainTex; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 uv = FlowUV(IN.uv_MainTex, _Time.y); fixed4 c = tex2D(_MainTex, uv) * _Color; o.Albedo = c.rgb; o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; }
当我们将两个坐标增加相同的量时,纹理将沿对角线滑动。 因为我们加上了时间,所以它从右上方滑动到左下方。 并且由于我们为纹理使用默认的环绕模式,因此动画每秒循环一次。
仅当时间值增加时,动画才可见。 当编辑器处于播放模式时就是这种情况,您也可以通过“场景”窗口工具栏启用“动画材质”来在编辑模式下启用时间进度。
启用动画材质
1.3 流动方向
您可以使用速度矢量来控制流动的方向和速度,而不必始终沿相同的方向流动。您可以将此矢量作为属性添加到材质。但是,我们仍然仅限于对整个材质使用相同的矢量,看起来像是刚性的滑动表面。为了使某种东西看起来像流动的液体,除了一般移动之外,它还必须随时间局部变化。
我们可以通过添加另一个速度矢量来消除静态外观,使用该速度矢量第二次对纹理进行采样,然后将两个采样组合在一起。当使用两个略有不同的矢量时,我们最终得到一个变形纹理。但是,我们仍然仅限于以相同方式流动整个表面。对于开阔水域或直流而言,这通常就足够了,但在更复杂的情况下却不足够。
为了支持更多有趣的流动,我们必须以某种方式改变整个材料表面的流动向量。最简单的方法是通过流动图。这是包含2D向量的纹理。这种纹理的R通道中具有矢量的U分量,G通道中具有V分量。它不需要很大,因为我们不需要急剧的突然变化,我们可以依靠双线性滤波来保持平滑。
流动贴图
该纹理是使用卷曲噪声创建的,在“噪波导数”教程中对此进行了说明,但是创建纹理的细节并不重要。 它包含多个顺时针和逆时针旋转流,没有任何源或汇。 确保将其导入为不是sRGB的常规2D纹理,因为它不包含颜色数据。
导入为非sRGB纹理
将流动图的属性添加到我们的材料中。 它不需要单独的UV平铺和偏移,因此请为其指定NoScaleOffset属性。 默认值为不会滚动,这对应于黑色纹理。
_MainTex ("Albedo (RGB)", 2D) = "white" {}
[NoScaleOffset] _FlowMap ("Flow (RG)", 2D) = "black" {}
带有流动图的材质
sampler2D _MainTex, _FlowMap; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg; float2 uv = FlowUV(IN.uv_MainTex, _Time.y); fixed4 c = tex2D(_MainTex, uv) * _Color; o.Albedo = c.rgb; o.Albedo = float3(flowVector, 0); … }
平铺的流向量
该纹理是线性数据,因此在场景中显得更亮。 无论如何我们都不应该流动图用作颜色值。 由于表面着色器的主要UV坐标使用主要纹理的平铺和偏移,因此我们的流动图也会平铺。 我们不需要平铺流动图,因此将材质的平铺值设置回1。
不带平铺的流向量
1.4 定向流动
现在我们有了流向量,我们可以在FlowUV函数中添加对它们的支持。 为它们添加一个参数,然后将它们乘以时间,再用原始UV减去它们。 这会使uv向矢量方向流动。
float2 FlowUV (float2 uv, float2 flowVector, float time) { return uv - flowVector * time; }
将流动向量传递给函数,但在执行此操作之前,请确保该向量有效。 与法线贴图一样,流动向量可以指向任何方向,因此可以包含负分量。 因此,矢量的编码方式与法线贴图相同。 我们必须手动对其进行解码。 同样,恢复到原始的反照率。
float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; float2 uv = FlowUV(IN.uv_MainTex, flowVector, _Time.y); fixed4 c = tex2D(_MainTex, uv) * _Color; o.Albedo = c.rgb; // o.Albedo = float3(flowVector, 0);
结果纹理过于扭曲。 发生这种情况是因为纹理在多个方向上移动,随着时间的推移越来越多地拉伸和挤压它。 为了防止它变成一团糟,我们必须在某个时候重置动画。 最简单的方法是仅使用动画时间的小数部分。 因此,它通常从0上升到1,然后重置为0,形成锯齿形。
锯齿状
float2 FlowUV (float2 uv, float2 flowVector, float time) { float progress = frac(time); return uv - flowVector * progress; }
现在纹理确实在不同的方向和以不同的速度变形。 除了突然的重置,最明显的是纹理随着变形的增加而迅速变得块状。 这是由于流动图压缩引起的。 默认压缩设置使用DXT1格式,这是块状性的来源。 这些伪影在使用有机纹理时通常不明显,但在使清晰的图案(如我们的测试纹理)变形时会刺眼。 因此,本教程中的所有屏幕截图和短片都使用了未压缩的流程图。
为什么不使用更高分辨率的流动图呢?
因为尽管这是可行的,但流图通常会覆盖较大的区域,因此最终导致的有效分辨率较低。 只要您不使用极端变形,就没有问题。 本教程中显示的变形非常强烈,以使其在视觉上显而易见。
2.无缝循环
2.1 混合权重
我们无法避免重置变形的进程,但是我们可以尝试隐藏它。 我们可以做的就是在接近最大失真时将纹理淡化为黑色。 如果我们也从黑色开始并且在开始时在纹理中淡入淡出,那么当整个表面为黑色时会突然重置。 尽管这很明显,但至少没有突然的视觉不连续。
为了使淡入淡出成为可能,让我们在FlowUV函数的输出中添加一个混合权重,并将其重命名为FlowUVW。 权重放在第三个成分中,到目前为止,有效成分实际上是1,所以让我们开始吧。
float3 FlowUVW (float2 uv, float2 flowVector, float time) { float progress = frac(time); float3 uvw; uvw.xy = uv - flowVector * progress; uvw.z = 1; return uvw; }
我们可以通过将其乘以可用的权重来使纹理渐变。
float3 uvw = FlowUVW(IN.uv_MainTex, flowVector, _Time.y); fixed4 c = tex2D(_MainTex, uvw.xy) * uvw.z * _Color;
2.2 跷跷板
现在我们必须创建一个权重公式 w(p),即w(0)=w(1)=0。到一半时,它应该达到最高点,即w(1/2)=1。符合这些条件的最简单函数是三角波,w(p)=1-|1-2p|,用它作用我们的权重。
锯齿与对应的三角波
uvw.z = 1 - abs(1 - 2 * progress);
为什么不使用更平滑的方式?
您也可以使用正弦波或应用smoothstep函数。 但是因为这些函数使着色器更加复杂,而对最终结果的影响不大。 三角波就足够了。
2.3 时间偏移
从技术上讲,我们已消除了视觉上的不连续性,但引入了黑脉冲效应。 脉冲非常明显,如果我们可以随着时间的流逝而传播它,可能会变得不太明显。我们可以通过在整个表面上以不同的时间偏移时间来做到这一点。一些低频的Perlin噪声非常适合于此, 无需添加其他纹理,而是将噪声打包在流动图中。 这里有与之前相同的流动图,但是现在多了噪声图,赋值在其A通道中。 噪声与流向量无关。
[NoScaleOffset] _FlowMap ("Flow (RG, A noise)", 2D) = "black" {}
采样噪声并将其添加到传递给FlowUVW之前的时间。
float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; float noise = tex2D(_FlowMap, IN.uv_MainTex).a; float time = _Time.y + noise; float3 uvw = FlowUVW(IN.uv_MainTex, flowVector, time);
黑色脉冲仍然存在,但是已经变成了在表面传播的波,这比均匀脉冲更容易混淆。另外,时间偏移还使扭曲的进行变得不均匀,从而导致总体上变化更大。
2.4 结合两种扭曲
我们可以不融合为黑色,而可以融合其他元素,例如原始的未扭曲的纹理。 但是,随后我们会看到固定的纹理淡入淡出,会有种破坏流动的错觉。 我们可以通过与其他扭曲的纹理融合来解决该问题。 这要求我们对纹理进行两次采样,每个采样具有不同的UVW数据。
因此,我们最终得到两个脉冲模式A和B。当A的权重为0时,B的权重应为1,反之亦然。 这样黑脉冲就被隐藏了。 这是通过将B的相位偏移其周期的一半来完成的,这意味着将其时间增加0.5。 但这是FlowUVW函数的工作方式的详细信息,因此,我们只需添加一个布尔参数来指示我们是否要为A或B变体使用UVW。
float3 FlowUVW (float2 uv, float2 flowVector, float time, bool flowB) { float phaseOffset = flowB ? 0.5 : 0; float progress = frac(time + phaseOffset); float3 uvw; uvw.xy = uv - flowVector * progress; uvw.z = 1 - abs(1 - 2 * progress); return uvw; }
A和B的权重总和为1
现在,我们必须两次调用FlowUVW,一次调用false,最后一次调用true。 然后对纹理采样两次,将它们的权重相乘,然后相加,得出最终的反照率。
float time = _Time.y + noise; float3 uvwA = FlowUVW(IN.uv_MainTex, flowVector, time, false); float3 uvwB = FlowUVW(IN.uv_MainTex, flowVector, time, true); fixed4 texA = tex2D(_MainTex, uvwA.xy) * uvwA.z; fixed4 texB = tex2D(_MainTex, uvwB.xy) * uvwB.z; fixed4 c = (texA + texB) * _Color;
黑色的脉冲波不再可见。 波浪仍然存在,但是现在形成了两个阶段之间的过渡,这已经不那么明显了。
在两个模式之间偏移一半周期的混合副作用是动画的持续时间减少了一半。 现在,它每秒循环两次。 但是我们不必两次使用相同的模式。 我们可以将B的UV坐标偏移半个单位。 这将使图案不同(同时使用相同的纹理),而不会引入任何方向偏差。
uvw.xy = uv - flowVector * progress + phaseOffset;
因为我们使用常规测试图案,所以A和B的白色网格线重叠。 但是他们的正方形的颜色是不同的。 结果,最终动画在两种颜色配置之间交替,并再次花费一秒钟重复。
2.5 UV跳跃
除了始终将A和B的UV偏移半个单位外,还可以按相位偏移UV。 这将导致动画随时间变化,因此需要更长的时间才能循环回到完全相同的状态。
我们可以简单地基于时间滑动UV坐标,但这将导致整个动画滑动,从而引入方向偏差。 我们可以通过在每个阶段保持UV偏移恒定,然后在各个阶段之间跳转到新的偏移来避免视觉滑动。 换句话说,每当权重为零时,我们就使UV跳跃。 这是通过在UV上加上一些跳跃偏移量乘以时间的整数部分来完成的。 调整FlowUVW以支持此功能,并使用新参数指定跳转向量。
float3 FlowUVW (float2 uv, float2 flowVector, float2 jump, float time, bool flowB) { float phaseOffset = flowB ? 0.5 : 0; float progress = frac(time + phaseOffset); float3 uvw; uvw.xy = uv - flowVector * progress + phaseOffset; uvw.xy += (time - progress) * jump; uvw.z = 1 - abs(1 - 2 * progress); return uvw; }
在我们的着色器中添加两个参数以控制跳转。 我们使用两个浮点数而不是单个向量,因此我们可以使用范围滑块。 因为我们在两个偏移一半的图案之间进行混合,所以我们的动画已经包含UV偏移序列每相位0->1/2。跳转偏移量被添加在此之上。这意味着如果我们跳到一半,进度将变为0->1/2->1/2->0每两个相位,这不是我们想要的。我们最多应该跳四分之一,这会产生0->1/2->1/4->3/4->1/2->0->3/4->1/4每4个相位。负偏移最多也可以达到四分之一。 那会产生顺序0->1/2->3/4->1/4->1/2->0->1/4->3/4.
[NoScaleOffset] _FlowMap ("Flow (RG, A noise)", 2D) = "black" {} _UJump ("U jump per phase", Range(-0.25, 0.25)) = 0.25 _VJump ("V jump per phase", Range(-0.25, 0.25)) = 0.25
将所需的float变量添加到我们的着色器中,使用它们构造跳转向量,并将其传递给FlowUVW。
sampler2D _MainTex, _FlowMap; float _UJump, _VJump; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; float noise = tex2D(_FlowMap, IN.uv_MainTex).a; float time = _Time.y + noise; float2 jump = float2(_UJump, _VJump); float3 uvwA = FlowUVW(IN.uv_MainTex, flowVector, jump, time, false); float3 uvwB = FlowUVW(IN.uv_MainTex, flowVector, jump, time, true); … }
在最大跳跃的情况下,在重复之前,我们将经历八个UV偏移的序列。 当我们每个阶段经历两个偏移并且每个阶段都长一秒时,我们的动画现在每四秒钟循环一次。
2.6 分析跳跃
为了更好地了解UV跳跃是如何工作的,可以将流矢量设置为零,以便集中于偏移量。 首先,考虑动画没有任何跳跃,只是原始的交替模式。
您可以看到每个正方形在两种颜色之间交替。 您还可以看到,我们在相同的纹理偏移量之间交替了一半,但这并不是立即显而易见的,也没有方向偏差。 接下来,看一下两个方向上跳动最大的动画。
结果看起来有所不同,因为跳跃四分之一会导致测试纹理的网格线移动,在正方形和十字形之间交替。 白线仍然没有显示方向偏差,但是彩色正方形现在可以了。 模式沿对角线移动,但不是立即可见。 向前走半步,然后向后走四分之一步,重复一次。 如果我们使用-0.25的最小跳跃,那么它将向前走半步,然后向前走四分之一步,重复一次。 为了使方向偏差更明显,请使用不对称的跳变,例如0.2。
在这种情况下,白色网格线也会移动。但是,由于我们仍在使用相当接近对称的大跳跃,因此可以将移动解释为向多个方向移动,具体取决于您对图像的聚焦方式。如果您改变焦点,则很容易就无法确定方向。
因为我们使用的是0.2的跳变,所以动画会在五个阶段(即五秒钟)之后重复播放。但是,由于我们在两个偏移相位之间进行混合,因此在每个相位的中间都有一个潜在的交叉点。如果动画将在奇数个相位后循环播放,则实际上会在相位相交一半时循环播放两次。因此,在这种情况下,持续时间仅为2.5秒。
您不必将U和V跳跃相同的数量。除了改变方向偏差的性质外,每个维度使用不同的跳变值还会影响环路持续时间。例如,考虑0.25的U跳和0.1的V跳。 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中完成,这意味着我们的函数需要一个平铺参数。
float3 FlowUVW ( float2 uv, float2 flowVector, float2 jump, float tiling, float time, bool flowB ) { … // uvw.xy = uv - flowVector * progress + phaseOffset; uvw.xy = uv - flowVector * progress; uvw.xy *= tiling; uvw.xy += phaseOffset; … }
也向我们的着色器添加一个平铺属性,默认值为1。
_UJump ("U jump per phase", Range(-0.25, 0.25)) = 0.25 _VJump ("V jump per phase", Range(-0.25, 0.25)) = -0.25 _Tiling ("Tiling", Float) = 1
然后添加所需的变量并将其传递给FlowUVW。
float _UJump, _VJump, _Tiling; … void surf (Input IN, inout SurfaceOutputStandard o) { … float3 uvwA = FlowUVW( IN.uv_MainTex, flowVector, jump, _Tiling, time, false ); float3 uvwB = FlowUVW( IN.uv_MainTex, flowVector, jump, _Tiling, time, true ); … }
当平铺设置为2时,动画的流动速度似乎是以前的两倍。 但这仅仅是因为纹理已缩放。 当不跳跃UV时,动画仍然需要一秒钟来循环播放。
3.2 动画速度
动画的速度可以通过缩放时间直接控制。 这会影响整个动画,并影响其持续时间。 添加一个速度着色器属性以支持此操作。
_Tiling ("Tiling", Float) = 1 _Speed ("Speed", Float) = 1
只需将_Time.y乘以相应的变量即可。 之后应添加噪声值,因此时间偏移不会受到影响。
float _UJump, _VJump, _Tiling, _Speed; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; float noise = tex2D(_FlowMap, IN.uv_MainTex).a; float time = _Time.y * _Speed + noise; … }
3.3 流动强度
流速由流图决定。 我们可以通过调整动画速度来加快或降低速度,但这也会影响相位长度和动画持续时间。 改变视在流速的另一种方法是缩放流向量。 通过调整流的强度,我们可以在不影响时间的情况下加快,减慢甚至逆转它。 这也改变了失真量。 添加“流动强度”着色器属性以使其成为可能。
_Speed ("Speed", Float) = 1 _FlowStrength ("Flow Strength", Float) = 1
在使用之前,只需将流向量乘以相应的变量即可。
float _UJump, _VJump, _Tiling, _Speed, _FlowStrength; … void surf (Input IN, inout SurfaceOutputStandard o) { float2 flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; flowVector *= _FlowStrength; … }
3.4 流动偏移
另一个可能的调整是控制动画的开始位置。 到现在为止,我们总是在每个阶段的开始时从零失真开始,逐渐发展到最大失真。 当相的权重在中点达到1时,当变形为一半强度时,图案最清晰。 因此,我们大多数情况下会看到半失真的纹理。 这种配置通常很好,但并非总是如此。 例如,在Portal 2中,漂浮的碎片纹理通常以其未变形的状态出现。 这是通过使UV坐标变形时使流量偏移-0.5来完成的。
通过向FlowUVW添加flowOffset参数,我们也支持这一点。 仅与流向量相乘时,将其添加到进度中。
float3 FlowUVW ( float2 uv, float2 flowVector, float2 jump, float flowOffset, float tiling, float time, bool flowB ) { float phaseOffset = flowB ? 0.5 : 0; float progress = frac(time + phaseOffset); float3 uvw; uvw.xy = uv - flowVector * (progress + flowOffset); uvw.xy *= tiling; uvw.xy += phaseOffset; uvw.xy += (time - progress) * jump; uvw.z = 1 - abs(1 - 2 * progress); return uvw; }
接下来,添加一个属性以控制着色器的流偏移量。 它的实际值为0和-0.5,但是您也可以尝试其他值。
_FlowStrength ("Flow Strength", Float) = 1 _FlowOffset ("Flow Offset", Float) = 0
将相应的变量传递给FlowUVW。
float _UJump, _VJump, _Tiling, _Speed, _FlowStrength, _FlowOffset; … void surf (Input IN, inout SurfaceOutputStandard o) { … float3 uvwA = FlowUVW( IN.uv_MainTex, flowVector, jump, _FlowOffset, _Tiling, time, false ); float3 uvwB = FlowUVW( IN.uv_MainTex, flowVector, jump, _FlowOffset, _Tiling, time, true ); … }
流量偏移为-0.5时,每个相位的峰值处都没有失真。 但是由于时间偏移,总体结果仍会失真。
4. 贴图
我们的失真流着色器现在可以正常使用了。 让我们看看到目前为止使用的测试纹理以外的其他东西的外观。
4.1 抽象的水
失真效果最常见的用途是模拟水表面。 但是因为变形可以在任何方向上进行,所以我们不能使用建议特定流动方向的纹理。 不建议方向就不可能做出正确的波浪,但是我们不需要现实。 当纹理变形和融合时,它只必须看起来像水。 例如,这是一个简单的噪声纹理,它结合了一个八度的低频Perlin和Voronoi噪声。 它是水的抽象灰度表示,波浪的底部是深色,波浪的顶部是浅色。
water texture
使用此纹理作为我们材料的反照率贴图。 除此之外,我没有使用跳跃,3的平铺,0.5的速度,0.1的流动强度以及无流量偏移。
即使噪声纹理本身看起来并不像水,失真和动画效果也开始看起来像水。 您还可以通过将流动强度临时设置为零来检查其外观如何而不会变形。 这将代表静止的水,并且看起来至少应该可以接受。
4.2 法线贴图
反照率图只是预览,因为流动的水主要由其表面垂直变化的方式定义,这改变了它与光的交互方式。 为此,我们需要一个法线贴图。 这是一个通过将反照率纹理解释为高度图而创建的图像,但是高度按0.1缩放,因此效果不太强。
法线贴图
为法线贴图添加一个着色器属性。
[NoScaleOffset] _FlowMap ("Flow (RG, A noise)", 2D) = "black" {} [NoScaleOffset] _NormalMap ("Normals", 2D) = "bump" {}
采样A和B的法线贴图,应用它们的权重,并将它们的归一化总和用作最终的表面法线。
sampler2D _MainTex, _FlowMap, _NormalMap; … void surf (Input IN, inout SurfaceOutputStandard o) { … float3 normalA = UnpackNormal(tex2D(_NormalMap, uvwA.xy)) * uvwA.z; float3 normalB = UnpackNormal(tex2D(_NormalMap, uvwB.xy)) * uvwB.z; o.Normal = normalize(normalA + normalB); fixed4 texA = tex2D(_MainTex, uvwA.xy) * uvwA.z; fixed4 texB = tex2D(_MainTex, uvwB.xy) * uvwB.z; … }
将法线贴图添加到我们的材料中。 还要将其平滑度提高到0.7左右,然后更改光线,以便获得大量的镜面反射。 我将视图保持不变,但是将定向光旋转了180°至(50,150,0)。 同时将反照率设置为黑色,因此我们只能看到法线动画的效果。
扭曲且动作化的法线贴图创建了令人信服的流水幻觉。 但是当流动强度为零时如何保持呢?
乍看起来似乎不错,但是如果您专注于特定的亮点,很快就会发现它们在两种状态之间交替。 幸运的是,这可以通过使用非零的跳转值来解决。
4.3 导数图
尽管生成的法线看起来不错,但对法线进行平均并没有多大意义。如Rendering 6 Bumpiness中所述,正确的方法是将法线向量转换为高度导数,将其相加,然后转换回法线向量。对于穿过表面传播的波尤其如此。
由于我们通常对法线贴图使用DXT5nm压缩,因此我们首先必须重建两个法线的Z分量(这需要平方根计算),然后转换为导数,合并并归一化。但是我们不需要原始的法线向量,因此我们也可以通过将导数存储在地图中而不是法线来跳过转换。
导数贴图的工作方式与法线贴图相同,不同之处在于它包含X和Y维度的高度导数。但是,没有额外的缩放比例,导数贴图只能支持最大45°的表面角度,因为该角度的导数为1。由于您通常不会使用这种陡峭的波浪,因此可以接受这种限制。
这是一个与以前的法线贴图描述相同曲面的导数贴图,就像法线贴图一样,X导数存储在A通道中,Y导数存储在G通道中。另外,它的B通道中还包含原始高度图。但是,通过将高度缩放0.1来再次计算导数。
导数加高度图
为什么不也存储以0.1缩放的高度?
高度数据将以最大强度存储,以最大程度地减少精度损失。
由于纹理不是法线贴图,因此将其导入为常规2D纹理。 确保指示它不是sRGB纹理。
将我们的导数加高度贴图的普通贴图着色器属性替换为一个。
// [NoScaleOffset] _NormalMap ("Normals", 2D) = "bump" {} [NoScaleOffset] _DerivHeightMap ("Deriv (AG) Height (B)", 2D) = "black" {}
还要替换着色器变量,采样和常规构造。 我们不能再使用UnpackNormal,因此创建一个自定义的UnpackDerivativeHeight函数,该函数将正确的数据通道放入浮点向量并解码导数。
sampler2D _MainTex, _FlowMap, _DerivHeightMap; … float3 UnpackDerivativeHeight (float4 textureData) { float3 dh = textureData.agb; dh.xy = dh.xy * 2 - 1; return dh; } void surf (Input IN, inout SurfaceOutputStandard o) { … // float3 normalA = UnpackNormal(tex2D(_NormalMap, uvwA.xy)) * uvwA.z; // float3 normalB = UnpackNormal(tex2D(_NormalMap, uvwB.xy)) * uvwB.z; // o.Normal = normalize(normalA + normalB); float3 dhA = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwA.xy)) * uvwA.z; float3 dhB = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwB.xy)) * uvwB.z; o.Normal = normalize(float3(-(dhA.xy + dhB.xy), 1)); … }
用导数图代替法线图。
生成的表面法线看起来几乎与使用法线贴图时的外观相同,但它们的计算成本较低。 由于我们现在也可以访问高度数据,因此我们也可以使用它来对表面着色。 这对于调试很有用,因此让我们暂时替换原始的反照率。
o.Albedo = c.rgb; o.Albedo = dhA.z + dhB.z;
使用高度作为反照率
该表面看起来比使用反照率纹理时更亮,即使两者都包含相同的高度数据。 有所不同,因为我们现在使用的是线性数据,而反照率纹理被解释为sRGB数据。 为了获得相同的结果,我们必须手动将高度数据从gamma转换为线性色彩空间。 我们可以通过简单地平方来近似。
o.Albedo = pow(dhA.z + dhB.z, 2);
4.4 高度比例
使用导数而不是法向量的另一个好处是可以轻松缩放它们。 派生的法线将与调整后的曲面匹配。 这使得可以正确地缩放波浪的高度。 让我们在着色器中添加一个height scale属性来支持这一点。
_FlowOffset ("Flow Offset", Float) = 0 _HeightScale ("Height Scale", Float) = 1
我们需要做的就是将高度比例因素分解到采样的导数加上高度数据中。
float _HeightScale; … void surf (Input IN, inout SurfaceOutputStandard o) { … float3 dhA = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwA.xy)) * (uvwA.z * _HeightScale); float3 dhB = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwB.xy)) * (uvwB.z * _HeightScale); … }
但是我们可以走得更远。 我们可以根据流速使高度比例可变。 这个想法是,当有强流量时,您会得到较高的波浪,而当有弱流量时,您将得到较低的波浪。 为了控制它,添加第二个高度比例属性,用于基于流速的调制高度。 另一个属性保持不变的规模。 最终的高度比例可以通过将两者结合起来找到。
_HeightScale ("Height Scale, Constant", Float) = 0.25 _HeightScaleModulated ("Height Scale, Modulated", Float) = 0.75
流速等于流速矢量的长度。 将其乘以调制比例,然后加上恒定比例,并将其用作导数加高度的最终比例。
float _HeightScale, _HeightScaleModulated; … void surf (Input IN, inout SurfaceOutputStandard o) { … float finalHeightScale = length(flowVector) * _HeightScaleModulated + _HeightScale; float3 dhA = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwA.xy)) * (uvwA.z * finalHeightScale); float3 dhB = UnpackDerivativeHeight(tex2D(_DerivHeightMap, uvwB.xy)) * (uvwB.z * finalHeightScale); … }
尽管您可以完全根据流速来确定高度比例,但最好至少使用一个较小的恒定比例,这样在没有流速的地方表面就不会变得平坦。 例如,使用0.1的恒定比例和9的调制比例。它们不需要加1,设置取决于最终法线的强度和所需的多样性。
4.5 流动加速度
与其在着色器中计算流速,不如将其存储在流程图中。 尽管采样过程中的滤波可以非线性地改变矢量的长度,但只有在对两个非常不同的矢量进行插值时,这种差异才会变得很明显。 只有在我们的流程图中方向突然改变时,情况才会如此。 只要我们没有这些,对存储的速度向量进行采样就会产生几乎相同的结果。 另外,调制高度比例时不一定要完全匹配。
这是与以前相同的流程图,但是现在将速度值存储在其B通道中。
B通道中具有速度的流图
使用采样数据而不是自己计算速度。 由于速度没有方向,因此不应进行转换,这与速度矢量不同。
void surf (Input IN, inout SurfaceOutputStandard o) { // float flowVector = tex2D(_FlowMap, IN.uv_MainTex).rg * 2 - 1; float3 flow = tex2D(_FlowMap, IN.uv_MainTex).rgb; flow.xy = flow.xy * 2 - 1; flow *= _FlowStrength; … float3 uvwA = FlowUVW( IN.uv_MainTex, flow.xy, jump, _FlowOffset, _Tiling, time, false ); float3 uvwB = FlowUVW( IN.uv_MainTex, flow.xy, jump, _FlowOffset, _Tiling, time, true ); float finalHeightScale = flow.z * _HeightScaleModulated + _HeightScale; … }
我们通过恢复原始反照率来结束。 我还将材质颜色更改为蓝色,具体是(78,131,169)。
// o.Albedo = pow(dhA.z + dhB.z, 2);
有可信度的水效果的最重要的品质是其动画表面法线的质量。 一旦这些效果很好,您就可以添加效果,例如更高级的反射,透明度和折射。 但是,即使没有这些附加功能,该表面也将被解释为水。