US3S9L4——顶点波动效果——流动的河流

顶点波动可以达到的目标

假设我们的目标是让一个矩形网格面片,通过顶点动画,实现出河流的效果。

image

image

image

所谓的河流效果,就是呈现出波浪感,而想要呈现出波浪感,我们必须了解波长、波动频率、波动幅度这些关键因素

波浪感的关键因素

  • 波长:指两个相邻波峰或波谷之间的距离。波长越大,波动越缓慢,波形周期越长。

    • 波长的倒数:1波长\frac{1}{波长},倒数越大,表示波动越频繁,波形周期越短
  • 波动频率:指波动在单位时间内发生的次数(相当于波浪变化的频率)

  • 波动幅度:指波峰或波谷相对于中线(静止位置)的最大偏移位置

image

image

我们需要在我们的 Shader 代码中,声明这三个关键因素变量,用于控制顶点的偏移,从而实现流动的 2D 河流效果

波浪感基本原理

基本原理:让我们的顶点在对应的轴向产生偏移。

主要运用的就是 Shader 中的内置函数 sin()​,以及内置时间变量 _Time.y

  • sin()​ 是正弦函数,正弦函数是一个周期性函数,常用与表示波动和震荡效果,它的返回值是 -1 ~ 1
  • _Time.y​ 是切换到当前场景后所经过的时间,参与到计算中,可以让我们的波浪周期性变化

再结合波长的倒数、波动评率、波动幅度等可变参数,参与到计算中,便可以实现效果

具体步骤:

  • 关键步骤一:让顶点上下动起来

    我们可以利用 sin​ 函数让顶点在希望移动的轴向上产生偏移,
    并且为了能够周期性变化,可以让时间参与到计算中:sin(_Time.y)​,该函数随着时间的变化,会不停地返回 -1 ~ 1 之间的数,
    为了控制波动评率,我们可以声明波动评率变量参与计算:sin(_Time.y * 波动频率)
    用得到的返回值,作为顶点在某一轴向的偏移值,便可以让顶点动起来

    问题:所有顶点偏移的会一样,会呈现出整体上下移动的效果

  • 关键步骤二:让顶点有差异性的动起来

    为了让顶点之间偏移位置有差异,我们可以在计算 sin()​ 时利用每个顶点的不同点制造差异性
    对于顶点来说,不同点主要来自坐标,我们可以利用他们变化**某个轴的坐标制造差异性:**​sin(_Time.y * 波动频率 + 顶点某轴坐标)
    用得到的返回值,作为顶点在某一轴向的偏移值,便可以让顶点有差异性的动起来

    问题:无法改变波长和波动幅度

  • 关键步骤三:体现波长和波动幅度

    sin(_Time.y * 波动频率 + 顶点某轴坐标)​ 已经可以帮助我们实现波动变化了,
    想要体现出波长和波动幅度,我们只需要将这两个变量参与计算即可

    • 波长的体现: sin(_Time.y * 波动频率 + 顶点某轴坐标 * 波长的倒数)​,倒数越大,波形周期越短
    • 波动幅度体现: sin(_Time.y * 波动频率 + 顶点某轴坐标 * 波长的倒数) * 波动幅度

    其中,某轴指的是与波本身平行的哪个轴,即横轴,以下图为例,X轴就是与波平行的那个轴

    image

    相当于将 -1~1 范围扩大了

流动的2D河流基本原理,就是利用下面这个公式,对顶点位置进行偏移计算:
纵轴位置的偏移量 = sin( _Time.y * 波动频率 + 顶点在横轴上的坐标 * 波长的倒数) * 波动幅度

其中:具体轴向根据模型空间决定,波动频率、波长倒数、波动幅度为自定义变量,可以外部调节

关闭批处理

渲染标签:“DisableBatching” = "True"​

主要作用:是否对 SubShader​ 关闭批处理

我们在制作顶点动画时,有时需要关闭该Shader的批处理,因为我们在制作顶点动画时,有时需要使用模型空间下的数据
而批处理会合并所有相关的模型,这些模型各自的模型空间会丢失,导致我们无法正确使用模型空间下相关数据

在实现流程的2D河流效果时,我们就需要让顶点在模型空间下进行偏移
因此我们需要使用该标签,为该 Shader 关闭批处理

流动的2D河流效果具体实现

以下图的面片为例,Z 轴是横轴,X 轴是纵轴,因此需要在 X 轴上偏移顶点,实现波动效果
(实际上一个模型要在哪个轴上波动,取决于此模型的本地坐标系是怎么样的)

image

  1. 新建 Shader,删除无用代码

  2. 声明属性、映射属性

    1. 主纹理(_MainTex​)
    2. 叠加的颜色(_Color​)
    3. 波动幅度(_WaveAmplitude​)
    4. 波动频率(_WaveFrequency​)
    5. 波长的倒数(_InvWaveLength​)
    1
    2
    3
    4
    5
    6
    7
    8
    Properties
    {
    _MainTex("Texture", 2D) = "white" {} // 主纹理
    _Color("Color", color) = (1, 1, 1, 1) // 叠加颜色
    _WaveAmplitude("WaveAmplitude", Float) = 1 // 波动幅度
    _WaveFrequency("WaveFrequency", Float) = 1 // 波动频率
    _InvWaveLength("InvWaveLength", Float) = 1 // 波长倒数
    }
  3. 透明 Shader 设置相关,并禁用批处理

    该模型纹理可能会有透明区域,因此需要使用透明混合相关的设置,具体详见:US3S3L5——透明度混合
    此外,由于该 Shader 会使用模型空间下的顶点位置,因此需要禁用批处理,防止其抛弃模型空间下顶点

    修改渲染标签的渲染类型为透明,队列为透明,并忽略投影机,同时禁用批处理
    关闭深度写入,开启混合,需要使用 SrcAlpha OneMinusSrcAlpha​ 混合

    1
    2
    3
    4
    5
    6
    7
    8
    Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True"}

    Pass
    {
    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha
    // ...
    }
  4. 结构体相关

    顶点和 uv

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct v2f
    {
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
    };

    sampler2D _MainTex;
    float4 _MainTex_ST;
    fixed4 _Color;
    float _WaveAmplitude;
    float _WaveFrequency;
    float _InvWaveLength;
  5. 顶点着色器

    利用理论中讲解的公式,计算对应轴向偏移位置,注意,需要在模型空间中偏移

    上图的例子中,Z 轴是横轴,X 轴是纵轴,因此我们要沿着模型的 Z 轴去偏移顶点 Y 轴上的位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    v2f vert (appdata_base v)
    {
    v2f o;
    // 模型空间下的偏移计算,假设此模型的横轴是Z轴,纵轴是Y轴,因此需要沿着模型的Z轴去偏移顶点Y轴上的位置
    float4 offset;
    offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InvWaveLength) * _WaveAmplitude;
    offset.yzw = float3(0, 0, 0);
    o.vertex = UnityObjectToClipPos(v.vertex + offset);
    // 计算uv坐标
    o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
    return o;
    }
  6. 片元着色器

    直接进行颜色采样,颜色叠加

    1
    2
    3
    4
    5
    6
    7
    fixed4 frag (v2f i) : SV_Target
    {
    // 从纹理采样并叠加颜色
    fixed4 color = tex2D(_MainTex, i.uv);
    color.rgb *= _Color.rgb;
    return color;
    }

显示效果(波动幅度0.2,波动频率0.3,波动倒数3):

image

现在,这条面片的顶点就在其纵轴上偏移了,而且会随着时间修改偏移量,实现了波动的效果

如果还要结合纹理滚动效果,可以在顶点着色器修改UV坐标来实现,具体可见:US3S9L3——滚动的背景

首先在属性处声明纹理滚动速度属性:

1
2
3
4
5
6
7
8
9
Properties
{
_MainTex("Texture", 2D) = "white" {} // 主纹理
_Color("Color", color) = (1, 1, 1, 1) // 叠加颜色
_WaveAmplitude("WaveAmplitude", Float) = 1 // 波动幅度
_WaveFrequency("WaveFrequency", Float) = 1 // 波动频率
_InvWaveLength("InvWaveLength", Float) = 1 // 波长倒数
_Speed("Speed", Float) = 1 // 纹理变化速度
}

然后在片元着色器上修改UV坐标,让其随着时间变化

1
2
3
4
5
6
7
8
9
10
11
12
13
v2f vert (appdata_base v)
{
v2f o;
// 模型空间下的偏移计算,假设此模型的横轴是Z轴,纵轴是Y轴,因此需要沿着模型的Z轴去偏移顶点Y轴上的位置
float4 offset;
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InvWaveLength) * _WaveAmplitude;
offset.yzw = float3(0, 0, 0);
o.vertex = UnityObjectToClipPos(v.vertex + offset);
// 计算uv坐标
o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv += float2(0, _Time.y * _Speed);
return o;
}

显示效果(注意,如果纹理没有滚动效果,可能是因为滚动方向和颜色变化方向不一致导致的,这时可以修改滚动方向或者更换贴图来解决):

image

完整 Shader 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Shader "TeachShader/Lesson94"
{
Properties
{
_MainTex("Texture", 2D) = "white" {} // 主纹理
_Color("Color", color) = (1, 1, 1, 1) // 叠加颜色
_WaveAmplitude("WaveAmplitude", Float) = 1 // 波动幅度
_WaveFrequency("WaveFrequency", Float) = 1 // 波动频率
_InvWaveLength("InvWaveLength", Float) = 1 // 波长倒数
_Speed("Speed", Float) = 1 // 纹理变化速度
}

SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True"}

Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;
float4 _MainTex_ST;
fixed4 _Color;
float _WaveAmplitude;
float _WaveFrequency;
float _InvWaveLength;
float _Speed;

v2f vert (appdata_base v)
{
v2f o;
// 模型空间下的偏移计算,假设此模型的横轴是Z轴,纵轴是Y轴,因此需要沿着模型的Z轴去偏移顶点Y轴上的位置
float4 offset;
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InvWaveLength) * _WaveAmplitude;
offset.yzw = float3(0, 0, 0);
o.vertex = UnityObjectToClipPos(v.vertex + offset);
// 计算uv坐标
o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv += float2(0, _Time.y * _Speed);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
// 从纹理采样并叠加颜色
fixed4 color = tex2D(_MainTex, i.uv);
color.rgb *= _Color.rgb;
return color;
}
ENDCG
}
}
}