US3S9L6——顶点动画的注意事项
US3S9L6——顶点动画的注意事项
顶点动画的注意事项 —— 批处理相关
在之前的顶点动画相关课程中一再强调,我们需要在渲染标签中添加 "DisableBatching"="True"
来让该 Shader 渲染的对象不进行批处理,目的是让基于模型空间的计算能够正确进行,不会影响最终的渲染结果
为什么批处理会影响顶点动画
Unity 中默认有静态批处理和动态批处理,批处理的主要作用是 合并多个对象,将他们作为一个 DrawCall 进行处理
之所以批处理会对顶点动画带来影响,是因为:不同的对象会拥有不同的变换矩阵(位置、旋转、缩放)
而批处理后,他们的变换矩阵会进行统一处理:
举例:
- 物体A:位于世界空间位置 (0, 0, 0),无旋转。
- 物体B:位于世界空间位置 (5, 0, 0),无旋转。
他们是两个独立的对象,拥有不同的变换矩阵,于是批处理与否带来的区别就是:
-
不进行批处理时:
每个对象的变换矩阵会单独传递给 Shader,顶点的模型空间位置会根据各自的变换进行正确计算
-
进行批处理时:
启用批处理后,Unity 会将对象 A 和对象 B 合并为一个 Draw Call,并使用一个统一的变换矩阵
比如在静态批处理中,Unity 会将对象 A 和对象 B 的顶点合并为一个网格,并使用统一的变换进行渲染
批处理后顶点位置是混合的,Shader 中无法区分不同对象的模型空间位置,因此可能带来的问题有:
-
顶点动画失效:
假设你希望顶点在模型空间的 x 方向上进行
sin 波动动画。
如果对象 A 和对象 B 的模型空间位置被混合,波动动画会变得不可预测 -
变换混淆:
对象 A 和对象 B 有不同的变换矩阵。
如果批处理后使用统一的变换矩阵,Shader 无法区分每个顶点属于哪个对象,导致所有顶点的动画效果混淆
总结:批处理会让对象失去独立性,相当于将多个对象之间独立的模型空间坐标系合并为一个坐标系
从而影响顶点的相对位置和变换矩阵等信息,导致顶点动画结果异常,因此我们通过渲染标签来关闭批处理
关闭批处理带来的问题
关闭批处理带来的最直接问题就是导致:
- DrawCall的提升
- DrawCall的提升可能会带来性能问题
如果 DrawCall 的增加并没有带来性能问题,那我们可以通过关闭批处理来解决顶点动画问题
如果带来了性能问题,并且必须优化带有顶点动画的Shader,我们应该如何解决呢?
如何解决关闭批处理带来的问题
首先 Shader 内需要开启批处理,然后采取如下的几种方案
-
顶点颜色
利用顶点颜色来存储每个顶点的位置信息或相对位置信息,
我们在 C# 代码中获取模型网格顶点数据,将数据存储到网格的颜色属性中
在 Shader 中通过颜色属性获取顶点信息1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19void Start()
{
// 获取网格组件
MeshFilter meshFilter = GetComponent<MeshFilter>();
if (meshFilter != null)
{
// 通过网格组件,即可得到网格信息,进而得到所有的顶点信息,这些顶点信息可以存储到颜色数组内
Mesh mesh = meshFilter.mesh;
Vector3[] vertices = mesh.vertices;
Color[] colors = new Color[vertices.Length];
for (int i = 0; i < vertices.Length; i++)
{
// 将模型空间位置存储在顶点颜色中
colors[i] = new Color(vertices[i].x, vertices[i].y, vertices[i].z, 1);
}
// 将颜色数组赋值到网格的颜色信息中即可
mesh.colors = colors;
}
}之后在 Shader 中,直接在
appdata_full 结构体中点出颜色成员color,即可利用它获取到顶点信息1
2
3
4
5
6v2f vert(appdata_full v)
{
v2f o;
float4 vertex = v.color;
// ...
} -
UV通道
和上面的顶点颜色方案类似,只是把相关信息存储到 UV 通道中而已,
但是一个 UV 通道只能存储Vector2[] 的值,因此可能会使用两个 UV 通道,
若使用两个 UV 通道,而第二个 UV 通道会只使用Vector2[] 的一个分量,因此 UV 通道的方法一般在存储两个值时使用
注意,Mesh 内可以点出多个 UV 通道,其中默认的 UV 通道mesh.uv不可使用,其他的 UV 通道只要未被占用就都可以使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22void Start()
{
// 获取网格组件
MeshFilter meshFilter = GetComponent<MeshFilter>();
if (meshFilter != null)
{
// 通过网格组件,即可得到网格信息,进而得到所有的顶点信息,这些顶点信息可以存储到Vector2数组内
Mesh mesh = meshFilter.mesh;
Vector3[] vertices = mesh.vertices;
Vector2[] uvData1 = new Vector2[vertices.Length];
Vector2[] uvData2 = new Vector2[vertices.Length];
for (int i = 0; i < vertices.Length; i++)
{
// 将模型空间位置存储在Vector2s数据内中
uvData1[i] = new Vector2(vertices[i].x, vertices[i].y);
uvData2[i] = new Vector2(vertices[i].z, 1);
}
// 将两个Vector2数组赋值到网格的其他UV通道内即可(只要此UV通道未被占用即可使用,不能使用第一个通道)
mesh.uv2 = new Vector2[uvData1.Length];
mesh.uv3 = new Vector2[uvData2.Length];
}
}之后在 Shader 中,直接在
appdata_full结构体中点出对应的 uv 通道,即可利用它获取到顶点信息1
2
3
4
5
6v2f vert(appdata_full v)
{
v2f o;
float4 vertex = float4(v.texcoord1.xy, v.texcoord2.xy);
// ...
}
等等
顶点动画的注意事项 —— 阴影相关
- 如何让对象投射阴影,详见:US3S6L3——让物体投射阴影
- 透明度混合物体投射阴影相关,详见:US3S6L8——透明度混合物体阴影实现
顶点动画物体投射阴影
我们可以为有顶点动画的物体,使用 LightMode(灯光模式)为 ShadowCaster(阴影投射)的 Pass(渲染通道),这样它便能投射阴影
但是如果我们直接使用内置的这种 Pass(默认 Shader 中的,通过 FallBack 寻找到的)投射的阴影会是不正确的,
因为默认 Pass 当中并不会使用新的顶点位置来投射,而是按照模型原来的顶点位置来计算阴影的,举例:
-
新建一个 Shader,复用流动的2D河流的 Shader 代码
具体详见:US3S9L4——顶点波动效果——流动的河流
-
为其加上一个不透明的 FallBack Shader 比如
VertexLit1
2
3
4
5
6
7Shader "TeachShader/Lesson97"
{
Properties {/*...*/}
SubShader {/*...*/}
Fallback "VertexLit"
} -
在
MeshRenderer中开启双面投射阴影
显示效果:

可见,这时我们使用该 Shader 投射出来的阴影是没有经过顶点动画变化的模型阴影
让顶点动画物体投射正确的阴影
想要让带有顶点动画的对象产生正确的阴影,我们需要自定义 投射阴影的 Pass(渲染通道),在其中加入对顶点的变换计算即可
我们需要自定义一个 LightMode(灯光模式)为 ShadowCaster(阴影投射)的 Pass(渲染通道),在顶点着色器函数中进行顶点相关的计算
-
基于上文创建的 Shader ,再复用基础阴影投射渲染通道代码中自行实现的阴影投射的Pass
代码详见:US3S6L3——让物体投射阴影 的 让物体投射阴影的部分
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// ShadowCaster Pass 投影阴影通道
pass
{
Tags { "LightMode" = "ShadowCaster" }
CGPROGRAM
// 告诉Unity编译器生成多个着色器变体,用于支持不同类型的阴影(SM,SSSM等等),确保着色器能够在所有可能的阴影投射模式下正确渲染
struct v2f
{
V2F_SHADOW_CASTER; // 顶点到片元着色器阴影投射结构体数据宏,定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器
};
v2f vert(appdata_base v)
{
v2f data;
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data); // 转移阴影投射器法线偏移宏,用于在顶点着色器中计算和传递阴影投射所需的变量
return data;
}
fixed4 frag(v2f i) : SV_Target
{
SHADOW_CASTER_FRAGMENT(i); //阴影投射片元宏,将深度值写入到阴影映射纹理中
}
ENDCG
} -
在该
Pass中加入 波形频率、波长的倒数、波形幅度 属性的映射 -
在该
Pass中的顶点着色器函数中 加入顶点的偏移计算(直接复制前面的代码) -
直接对模型空间中顶点进行偏移,不用进行裁剪坐标空间变换以及 UV 相关计算
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20float _WaveAmplitude;
float _WaveFrequency;
float _InvWaveLength;
struct v2f
{
V2F_SHADOW_CASTER; // 顶点到片元着色器阴影投射结构体数据宏,定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器
};
v2f vert(appdata_base v)
{
v2f data;
// 模型空间下的偏移计算,假设此模型的横轴是Z轴,纵轴是Y轴,因此需要沿着模型的Z轴去偏移顶点Y轴上的位置
float4 offset;
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InvWaveLength) * _WaveAmplitude;
offset.yzw = float3(0, 0, 0);
v.vertex += offset; // 由于下面的语句会自动执行裁剪空间的转换,因此这里只需要去偏移顶点即可
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data); // 转移阴影投射器法线偏移宏,用于在顶点着色器中计算和传递阴影投射所需的变量
return data;
}
显示效果:

可见,通过自定义投影阴影通道的逻辑,这时我们使用该 Shader 投射出来的阴影就是经过顶点动画变化的模型阴影
完整 Shader 如下:
1 | Shader "TeachShader/Lesson97" |
