US5L14——表面着色器实现动态液体
US5L14——表面着色器实现动态液体
动态液体效果
在游戏开发中,动态液体效果就是用 Shader 模拟出透明容器装透明液体的效果
这种效果常用与游戏和动画中,比如用于制作玻璃瓶装液体的效果
动态液体效果的基本原理
一句话概括它的基本原理:动态液体效果是通过透明渲染、像素剔除、波纹效果模拟来实现的。 其中的关键点为:
-
如何被容器装载
用两个模型,一个容器模型,一个液体模型,液体模型其实和容器模型一模一样,只是稍小一些,
让这两个模型使用不同的材质。容器使用透明材质,液体使用动态液体材质即可 -
如何透明渲染
在表面着色器中实现透明渲染,和顶点/片元着色器的透明度混合基本一致,
只需要通过设置渲染类型、队列、混合模式、关闭深度写入即可1
2
3Tags { "RenderType"="Transparent" "Queue"="Transparent" } // 设置渲染类型和队列为透明
Blend DstColor SrcColor // 设置混合模式
ZWrite Off // 为了实现透明,关闭深度写入 -
如何剔除像素
我们将模型空间中心点作为参考点,将其转换到世界空间下,再用模型当前世界空间下的点和它进行减法运算。
如果判断点在参考点上方的我们便对其进行剔除,这时我们可以加入自定义变量控制液面高度_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,就裁剪掉此像素不渲染 -
如何模拟波纹效果
我们可以利用之前学习的流动的河流的相关公式来计算波纹效果,具体详见: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时,像素一定被剔除
动态液体效果 具体实现
-
新建表面着色器
DynamicLiquid
(动态液体) -
删除不必要的代码
-
声明属性以及属性映射
- 液体颜色
_Color
- 高光颜色和光滑度(
rgb
做颜色,a
做光滑度)_Specular
- 液体高度
_Height
- 波纹变化速度
_Speed
- 波动幅度
_WaveAmplitude
- 波动频率
_WaveFrequency
- 波长的倒数
_InvWaveLength
1
2
3
4
5
6
7
8
9
10Properties
{
_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 // 波长倒数
} - 液体颜色
-
透明混合相关设置,最好启用双面渲染
1
2
3
4Tags { "RenderType"="Transparent" "Queue"="Transparent" } // 设置渲染类型和渲染队列为透明
Blend DstColor SrcColor // 混合相关设置
ZWrite Off // 关闭深度写入
Cull Off // 关闭剔除,双面渲染 -
编译指令设置
光照模型我们使用
StandardSpecular
,并且不要阴影noshadow
,同时surf
函数输出结构体参数改为SurfaceOutputStandardSpecular
1
2
3
4
void surf (Input IN, inout SurfaceOutputStandardSpecular o) { } -
输入结构体
只需要当前像素的世界坐标位置
1
2
3
4struct Input
{
float3 worldPos; // 世界空间下像素点的位置
}; -
实现表面函数
- 模型中心点转世界坐标
- 计算中心点和像素点 y 轴坐标差
- 像素剔除
- 波纹效果偏移计算
- 漫反射颜色、高光颜色、光滑度设置(因为这里只是单纯的颜色rgb通道混合,因此不需要设置透明度)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void 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 | Shader "TeachShader/DynamicLiquid" |
动态液体效果 的使用
-
创建两个胶囊体,一大一小,小的做为大的子对象
-
大的胶囊体,用 Unity 自带 Shader 设置为透明的,并设置光滑度为1,类似玻璃容器
-
小的胶囊体,用动态液体效果制作为容器中的液体
显示效果: