顶点动画
零、前置知识
是否对SubShader进行批处理 Tags{"DisableBatching" = "True/false"} 我们在制作顶点动画时,有时需要关闭该Shader的批处理 因为我们在制作顶点动画的时候,有时需要使用模型空间下的数据 而批处理会合并所有相关的模型,这些模型各自的模型空间会消失,导致我们无法正确使用模型空间下的相关数据
一、2D河流动画
1.基本原理
利用类似于机械波的波函数公式,对顶点进行位置偏移 某轴的位置偏移量 = 波动幅度 * sin(_Time.y * 波动频率 + 顶点某位置坐标 * 波长倒数)
其中,具体轴向根据模型空间决定
波动幅度、波动频率、波长倒数为可调节参数
注意:
在实现2D河流效果时,我们需要让顶点在模型空间下进行偏移
需要关闭批处理 Tags{"DisableBatching" = "false"} 观察资源模型空间的的轴向,是否符合Unity轴向标准
2.基本实现思路
- 属性声明、映射
- 主纹理
- 叠加颜色
- 波动幅度
- 波动频率
- 波长的倒数
- 透明Shader相关
- 渲染标签相关
Tags{"RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProject" = "True" "DisableBatching" = "false"} - 关闭深度写入、透明度混合
ZWrite off | Blend SrcAlpha OneMinusSrcAlpha
- 渲染标签相关
- 结构体相关
顶点、uv - 顶点着色器
利用公式计算对应轴向偏移位置(模型空间中偏移) - 片元着色器
直接进行颜色采样和颜色叠加
3.实现示例
1 | Shader "Unlit/2DWater" |
二、广告牌效果
1.基本原理
广告牌效果 是一种图形技术
用于确保对象(通常是二维纹理面片 或者 精灵(Sprite)图片) 始终面向摄像机
- 全向广告牌效果
对象所在的Z轴始终面向摄像机
适用与烟雾、火焰等效果 - 垂直广告牌效果
对象在一个特定轴向上保持固定方向,而在其他轴上面向摄像机
适用于 树木、花草、人物等需要在特定轴上保持正确方向的效果
基本原理: 旋转模型模型空间坐标系让其始终面向摄像机
- 其中最重要的就是计算三个轴向
Z轴: normal = 将摄像机位置转移到模型空间下 - 模型空间下新轴向的中心点
原Y轴:oldUP = 模型空间下的Y轴(0,1,0)
X轴: right = cross(normal,oldUp)
新Y轴: newUp = cross(normal,right) - 计算模型空间下新的顶点位置
- 偏移位置 = 顶点坐标 - Center
- 新顶点位置 = Center + X轴 * 偏移位置.x + Y轴 * 偏移位置.y + Z轴 * 偏移位置.z ### 2.实现思路
- 声明属性,属性映射
主纹理、颜色叠加、垂直广告牌程度(0为垂直广告牌,1为全向广告牌) - 透明Shader相关
注意:关闭批处理,并让其两面渲染 - 结构体相关
顶点和纹理坐标 - 顶点着色器
4-1:确定新坐标中心点
4-2:计算z轴(normal),将摄像机坐标转到模型空间
4-3:用垂直广告牌程度改变z轴y值后,单位化
4-4:声明Y轴(old up)
4-5:利用Z轴(normal)和Y轴(old up)叉乘计算出X轴(right)
4-6:利用z轴(normal)和X轴(right)叉乘计算出Y轴(up)
4-7:得到顶点相对于新坐标系中心点的偏移位置
4-8:利用新中心点和3轴计算出顶点新位置
4-9:新顶点转到裁剪空间
4-8:UV缩放偏移 - 片元着色器
直接采样 叠加颜色即可
3.实现示例
1 | Shader "Unlit/Billboarding" |
三、注意事项
1.批处理相关
1.)为什么批处理会影响顶点动画
Unity中默认有静态批处理和动态批处理
批处理主要作用:
合并多个对象,将他们作为一个DrawCall进行处理
之所以批处理会影响顶点动画
是因为
不同对象拥有不同的变换矩阵(位置、渲染、缩放)
而批处理之后
他们的变换矩阵会统一进行处理
举例:
物体A:位于世界空间位置(0,0,0),无旋转
物体B:位于世界空间位置(5,0,0),无旋转
他们是两个独立的对象,拥有不同的变换矩阵
- 不进行批处理时
每个对象的变换矩阵会单独传递给Shader,顶点的模型空间会根据各自的变换进行正确计算 - 进行批处理时:
启用批处理后,Unity会将对象A和对象B合并为一个DrawCall,并使用一个统一的变换矩阵
比如在静态批处理中,Unity会将对象A和对象B的顶点合并为一个网格,并使用统一的变换进行渲染
批处理后顶点的位置是混合的,Shader中无法区分不同对象的模型空间位置
可能带来的问题:- 顶点动画失效;
假设你希望顶点在顶点模型空间的x方向上进行sin波动动画
如果对象A和对象B的模型空间位置进行混合,波动的动画会变得不可预测 - 变换混淆
对象A和B有不同的变换举证
如果批处理后使用统一的变换矩阵,Shader无法区分每一个顶点属于那个对象,导致所有顶点动画的效果混淆
- 顶点动画失效;
- 总之
批处理会让对象失去独立性
相当于将多个对象之间独立的模型空间坐标系合并为一个坐标系
从而影响顶点的相对位置和变换矩阵等信息
导致顶点动画的异常
2.)关闭批处理带来的问题
关闭批处理带来的最直接的问题就是导致DrawCall的提升
从而导致性能的问题
3.)如何解决问题
开启批处理 1. 顶点颜色 利用顶点颜色来存储每一个顶点的位置信息或相对位置信息 我们在C#代码中获取模型网格顶点数据,将数据存储到网的颜色属性中 在Shader中通过颜色属性获取顶点信息 在Shader中直接在appdata_full结构体中点出颜色成员可以利用它获取到顶点信息 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16void Start()
{
MeshFilter meshFilter = GetComponent<MeshFliter>();
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++)
{
//将模型空间存储到颜色中
color[i] = new Color(vertices.x,vertices.y,vertices.z,1);
}
}
mesh.colors = colors;
}
2.阴影相关
由于投射阴影的逻辑较为通用
一般直接通过 FallBack "Specular"等直接使用内置的Pass 调用 Unity 中默认 Shader 中的相关代码即可 但是这种投射的阴影,对于顶点动画来说是不正确的 因为默认的Pass当中并不会使用新的顶点来投射,而是使用原来的顶点进行计算阴影
所以这种情况我们通过自己写一个阴影投射的Pass,来实现正确的效果
阴影投射的方法 主要是三个关键点 - 一个编译指令 #pragma multi_compile_shadowcaster - 一个内置文件 - 三个关键宏 这三个宏存储于#include "UnityCG.cginc"中
- V2F_SHADOW_CASTER
顶点到片元着色器阴影投射结构体数据宏
这个宏定义了一些标准的成员变量
这些变量用于在阴影投射路径中传递顶点数据到片元着色器
主要在结构体中使用 - TRANSFER_SHADOW_CASTER_NORMALOFFSET
转移阴影投射器法线偏移宏
用于在顶点着色器中计算和传递阴影投射所需的变量
主要做了:- 将对象空间的顶点位置转换到裁剪空间中
- 考虑法线偏移,以减轻阴影失真问题,尤其指在处理子阴影时
- 传递顶点的投影空间位置,用于后续的阴影计算 主要在顶点着色器中使用
- SHADOW_CASTER_FRAGMENT
阴影投射片元宏
将深度值写入到阴影映射纹理中
主要在片元着色器中使用
主要修改内容: 在将结构体传入TRANSFER_SHADOW_CASTER_NORMALOFFSET之前,对顶点在模型空间下的顶点坐标进行偏移(不用进行裁剪空间的转换)
示例 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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82Shader "Unlit/2DWaterShadow"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color ("Color", Color) = (1,1,1,1)
//波动幅度
_WaveAmplitude ("WaveAmplitude", Float) = 1
//波动频率
_WaveFrequency ("WaveFrequency", Float) = 1
//波长的倒数
_InWaveLength ("InWaveLength", Float) = 1
//纹理变化速度
_Speed ("Speed", Float) = 1
}
SubShader
{
//透明Shader渲染相关渲染标签 + 关闭批处理
Tags { "RenderType"="Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True" "DisableBatching" = "false"}
//实现顶点动画的Pass
Pass
{
//此处省略
}
//该Pass主要是用于进行阴影投影 主要是用来计算阴影映射投影的
Pass
{
Tags{"LightMode" = "ShadowCaster"}
CGPROGRAM
//该编译指令是告诉Unity编译器生成多个着色器变体
//用于支持不同类型的阴影(SM,SSM等)
//可以确保着色器能够在所有可能的阴影投射模式下正确渲染
//这个内置文件包含了关键的阴影计算相关的宏
struct v2f
{
//顶点到片元着色器 阴影投影结构体的数据宏
//这个宏定义了一些标准的成员变量,用于在阴影投射路径中传递顶点数据到片元着色器
V2F_SHADOW_CASTER;
};
float _WaveAmplitude;
float _WaveFrequency;
float _InWaveLength;
v2f vert(appdata_base v)
{
v2f data;
//模型空间下的偏移位置
float4 offset;
//这里使用的2D流水模型 上下是x轴,左右是z轴 前后是y轴
//这里让它进在模型空间下的x进行偏移
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InWaveLength) * _WaveAmplitude;
offset.yzw = float3(0,0,0);
//进行模型空间下的顶点偏移 直接在模型空间下进行计算
v.vertex = v.vertex + offset;
//转移阴影投射器法线偏移宏
//用于在顶点着色器中计算和传递投影所需的变量
//主要做了
//1. 将对象空间的顶点位置转化到裁剪空间
//2. 考虑法线偏移,以减轻阴影失帧问题,尤其是在处理自阴影时
//3. 传递顶点的投影空间位置,用于后续的阴影计算
TRANSFER_SHADOW_CASTER_NORMALOFFSET(data);
return data;
}
float4 frag(v2f i):SV_TARGET
{
//阴影投射片元宏
//将深度值写入到阴影映射纹理中
//我们主要在片元着色器中使用
SHADOW_CASTER_FRAGMENT(i);
}
ENDCG
}
}
}