US5L14——表面着色器实现动态液体

动态液体效果

在游戏开发中,动态液体效果就是用 Shader 模拟出透明容器装透明液体的效果
这种效果常用与游戏和动画中,比如用于制作玻璃瓶装液体的效果

imageimage

动态液体效果的基本原理

一句话概括它的基本原理:动态液体效果是通过透明渲染、像素剔除、波纹效果模拟来实现的。 其中的关键点为:

  1. 如何被容器装载

    用两个模型,一个容器模型,一个液体模型,液体模型其实和容器模型一模一样,只是稍小一些,
    让这两个模型使用不同的材质。容器使用透明材质,液体使用动态液体材质即可

  2. 如何透明渲染

    在表面着色器中实现透明渲染,和顶点/片元着色器的透明度混合基本一致,
    只需要通过设置渲染类型、队列、混合模式、关闭深度写入即可

    1
    2
    3
    Tags { "RenderType"="Transparent" "Queue"="Transparent" }    // 设置渲染类型和队列为透明
    Blend DstColor SrcColor // 设置混合模式
    ZWrite Off // 为了实现透明,关闭深度写入
  3. 如何剔除像素

    我们将模型空间中心点作为参考点,将其转换到世界空间下,再用模型当前世界空间下的点和它进行减法运算。
    如果判断点在参考点上方的我们便对其进行剔除,这时我们可以加入自定义变量控制液面高度 _Level​。

    1
    2
    3
    // 液面效果
    float3 pivot = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)); // 将物体的模型空间的中心点坐标(0,0,0)转换到对应的世界空间下坐标,用它来判断裁剪
    float liquid = pivot.y - IN.worldPos.y + _Level * 0.01; // 计算液体表面相对于当前点的高度差,_Level是控制液面高低的参数
    1
    2
    3
    // 像素剔除
    liquid = step(0, liquid); // 若liuqid >= 0,则返回1,否则返回0,控制liuqid为正数
    clip(liquid - 0.001); // 若liuqid - 0.001 < 0,就裁剪掉此像素不渲染
  4. 如何模拟波纹效果

    我们可以利用之前学习的流动的河流的相关公式来计算波纹效果,具体详见:US3S9L4——顶点波动效果——流动的河流
    即:纵轴位置的偏移量 = sin( _Time.y * 波动频率 + 顶点在横轴上的坐标 * 波长的倒数) * 波动幅度

    1
    2
    3
    4
    5
    6
    // 波纹效果
    float ripple = sin(_Time.y * _WaveFrequency + IN.worldPos.x * _InvWaveLength) * _WaveAmplitude;
    liquid += ripple;
    // 像素剔除
    liquid = step(0, liquid); // 若liuqid >= 0,则返回1,否则返回0,控制liuqid为正数
    clip(liquid - 0.001); // 若liuqid - 0.001 < 0,就裁剪掉此像素不渲染,-0.001 是确保为liuqid为0时,像素一定被剔除

动态液体效果 具体实现

  1. 新建表面着色器 DynamicLiquid​(动态液体)

  2. 删除不必要的代码

  3. 声明属性以及属性映射

    • 液体颜色 _Color
    • 高光颜色和光滑度(rgb​做颜色,a​ 做光滑度)_Specular
    • 液体高度 _Height
    • 波纹变化速度 _Speed
    • 波动幅度 _WaveAmplitude
    • 波动频率 _WaveFrequency
    • 波长的倒数 _InvWaveLength
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Properties
    {
    _Color("Color", Color) = (1, 1, 1, 1) // 液体颜色
    _Specular("Specular", Color) = (0, 0, 0, 0) // 高光颜色(rgb)和光滑度(a)
    _Height("Height", Float) = 0 // 液体高度
    _Speed("Speed", Float) = 1 // 波纹变化速度
    _WaveAmplitude("WaveAmplitude", Float) = 1 // 波动幅度
    _WaveFrequecy("WaveFrequecy", Float) = 1 // 波动频率
    _InvWaveLength("InvWaveLength", Float) = 1 // 波长倒数
    }
  4. 透明混合相关设置,最好启用双面渲染

    1
    2
    3
    4
    Tags { "RenderType"="Transparent" "Queue"="Transparent" }   // 设置渲染类型和渲染队列为透明
    Blend DstColor SrcColor // 混合相关设置
    ZWrite Off // 关闭深度写入
    Cull Off // 关闭剔除,双面渲染
  5. 编译指令设置

    光照模型我们使用 StandardSpecular​,并且不要阴影 noshadow​,同时 surf​ 函数输出结构体参数改为 SurfaceOutputStandardSpecular

    1
    2
    3
    4
    #pragma surface surf StandardSpecular noshadow
    #pragma target 3.0

    void surf (Input IN, inout SurfaceOutputStandardSpecular o) { }
  6. 输入结构体

    只需要当前像素的世界坐标位置

    1
    2
    3
    4
    struct Input
    {
    float3 worldPos; // 世界空间下像素点的位置
    };
  7. 实现表面函数

    1. 模型中心点转世界坐标
    2. 计算中心点和像素点 y 轴坐标差
    3. 像素剔除
    4. 波纹效果偏移计算
    5. 漫反射颜色、高光颜色、光滑度设置(因为这里只是单纯的颜色rgb通道混合,因此不需要设置透明度)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void surf (Input IN, inout SurfaceOutputStandardSpecular o)
    {
    // 计算此像素与液面的高度差
    float3 centerPoint = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)); // 将模型空间下中心点转换到世界空间下
    float liquidHeight = centerPoint.t - IN.worldPos.y + _Height * 0.01; // 当前像素点和中心点的高度差
    // 计算液面波纹偏移
    float waveOffset = sin(_Time.y * _WaveFrequecy + IN.worldPos.x * _InvWaveLength) * _WaveAmplitude;
    liuqidHeight += waveOffset;
    // 若此像素的高度差小于0,则剔除(step返回0,再减去0.001确保此像素必定被剔除),否则就继续渲染
    liquidHeight = step(0, liquidHeight);
    clip(liuqidHeight - 0.001);

    o.Albedo = _Color.rgb; // 漫反射颜色
    o.Specular = _Specular.rgb; // 高光颜色
    o.Smoothness = _Specular.a; // 光滑度
    }

完整 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
Shader "TeachShader/DynamicLiquid"
{
Properties
{
_Color("Color", Color) = (1, 1, 1, 1) // 液体颜色
_Specular("Specular", Color) = (0, 0, 0, 0) // 高光颜色(rgb)和光滑度(a)
_Height("Height", Float) = 0 // 液体高度
_Speed("Speed", Float) = 1 // 波纹变化速度
_WaveAmplitude("WaveAmplitude", Float) = 0.1 // 波动幅度
_WaveFrequecy("WaveFrequecy", Float) = 0.5 // 波动频率
_InvWaveLength("InvWaveLength", Float) = 1 // 波长倒数
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" } // 设置渲染类型和渲染队列为透明
Blend DstColor SrcColor // 混合相关设置
ZWrite Off // 关闭深度写入
Cull Off // 关闭剔除,双面渲染

CGPROGRAM
#pragma surface surf StandardSpecular noshadow
#pragma target 3.0

fixed4 _Color;
fixed4 _Specular;
float _Height;
float _Speed;
float _WaveAmplitude;
float _WaveFrequecy;
float _InvWaveLength;

struct Input
{
float3 worldPos; // 世界空间下像素点的位置
};

void surf (Input IN, inout SurfaceOutputStandardSpecular o)
{
// 计算此像素与液面的高度差
float3 centerPoint = mul(unity_ObjectToWorld, float4(0, 0, 0, 1)); // 将模型空间下中心点转换到世界空间下
float liquidHeight = centerPoint.y - IN.worldPos.y + _Height * 0.01; // 当前像素点和中心点的高度差
// 计算液面波纹偏移
float waveOffset = sin(_Time.y * _WaveFrequecy + IN.worldPos.x * _InvWaveLength) * _WaveAmplitude;
liquidHeight += waveOffset;
// 若此像素的高度差小于0,则剔除(step返回0,再减去0.001确保此像素必定被剔除),否则就继续渲染
liquidHeight = step(0, liquidHeight);
clip(liquidHeight - 0.001);

o.Albedo = _Color.rgb; // 漫反射颜色
o.Specular = _Specular.rgb; // 高光颜色
o.Smoothness = _Specular.a; // 光滑度
}
ENDCG
}
FallBack "Diffuse"
}

动态液体效果 的使用

  1. 创建两个胶囊体,一大一小,小的做为大的子对象

  2. 大的胶囊体,用 Unity 自带 Shader 设置为透明的,并设置光滑度为1,类似玻璃容器

    image

  3. 小的胶囊体,用动态液体效果制作为容器中的液体

    image

显示效果:

image