Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

顶点动画

零、前置知识


是否对SubShader进行批处理 Tags{"DisableBatching" = "True/false"}
我们在制作顶点动画时,有时需要关闭该Shader的批处理
因为我们在制作顶点动画的时候,有时需要使用模型空间下的数据
而批处理会合并所有相关的模型,这些模型各自的模型空间会消失,导致我们无法正确使用模型空间下的相关数据

一、2D河流动画

1.基本原理

利用类似于机械波的波函数公式,对顶点进行位置偏移
某轴的位置偏移量 = 波动幅度 * sin(_Time.y * 波动频率 + 顶点某位置坐标 * 波长倒数)
其中,具体轴向根据模型空间决定
波动幅度、波动频率、波长倒数为可调节参数

注意:
在实现2D河流效果时,我们需要让顶点在模型空间下进行偏移
需要关闭批处理 Tags{"DisableBatching" = "false"} 观察资源模型空间的的轴向,是否符合Unity轴向标准

2.基本实现思路

  1. 属性声明、映射
    1. 主纹理
    2. 叠加颜色
    3. 波动幅度
    4. 波动频率
    5. 波长的倒数
  2. 透明Shader相关
    1. 渲染标签相关
      Tags{"RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProject" = "True" "DisableBatching" = "false"}
    2. 关闭深度写入、透明度混合
      ZWrite off | Blend SrcAlpha OneMinusSrcAlpha
  3. 结构体相关
    顶点、uv
  4. 顶点着色器
    利用公式计算对应轴向偏移位置(模型空间中偏移)
  5. 片元着色器
    直接进行颜色采样和颜色叠加

3.实现示例

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
Shader "Unlit/2DWater"
{
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
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"

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

sampler2D _MainTex;
float4 _MainTex_ST;
float4 _Color;
float _WaveAmplitude;
float _WaveFrequency;
float _InWaveLength;
float _Speed;
v2f vert (appdata_base v)
{
v2f o;
//模型空间下的偏移位置
float4 offset;
//这里使用的2D流水模型 上下是x轴,左右是z轴 前后是y轴
//这里让它进在模型空间下的x进行偏移
offset.x = sin(_Time.y * _WaveFrequency + v.vertex.z * _InWaveLength) * _WaveAmplitude;
offset.yzw = float3(0,0,0);
o.vertex = UnityObjectToClipPos(v.vertex + offset);
//o.uv = v.texcoord * _MainTex_ST.xy + _MainTex_ST.zw;
o.uv = TRANSFORM_UV(v.texcoord);
//结合纹理变化
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
}
}
}

二、广告牌效果

1.基本原理

广告牌效果 是一种图形技术
用于确保对象(通常是二维纹理面片 或者 精灵(Sprite)图片) 始终面向摄像机

  • 全向广告牌效果
    对象所在的Z轴始终面向摄像机
    适用与烟雾、火焰等效果
  • 垂直广告牌效果
    对象在一个特定轴向上保持固定方向,而在其他轴上面向摄像机
    适用于 树木、花草、人物等需要在特定轴上保持正确方向的效果

基本原理: 旋转模型模型空间坐标系让其始终面向摄像机

  1. 其中最重要的就是计算三个轴向
    Z轴: normal = 将摄像机位置转移到模型空间下 - 模型空间下新轴向的中心点
    原Y轴:oldUP = 模型空间下的Y轴(0,1,0)
    X轴: right = cross(normal,oldUp)
    新Y轴: newUp = cross(normal,right)
  2. 计算模型空间下新的顶点位置
    1. 偏移位置 = 顶点坐标 - Center
    2. 新顶点位置 = Center + X轴 * 偏移位置.x + Y轴 * 偏移位置.y + Z轴 * 偏移位置.z ### 2.实现思路
  3. 声明属性,属性映射
    主纹理、颜色叠加、垂直广告牌程度(0为垂直广告牌,1为全向广告牌)
  4. 透明Shader相关
    注意:关闭批处理,并让其两面渲染
  5. 结构体相关
    顶点和纹理坐标
  6. 顶点着色器
    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缩放偏移
  7. 片元着色器
    直接采样 叠加颜色即可

3.实现示例

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
Shader "Unlit/Billboarding"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_Color("Color", Color) = (1,1,1,1)
//用于控制垂直广告牌和全向广告牌的变化
_VerticalBillboarding("VerticalBillboarding", Range(0,1)) = 1
}
SubShader
{
Tags { "RenderType"="Transparent" "Queue"="Transparent" "IgnoreProjector"="True" "DisableBatching"="True" }

Pass
{
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Cull Off

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 _VerticalBillboarding;

v2f vert (appdata_base v)
{
v2f o;
//新坐标系的中心点(默认我们使用的还是模型空间点的原点)
float3 center = float3(0,0,0);
//计算Z轴的方向
float3 cameraInObjectPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
//得到Z轴对应的向量
float3 normalDir = cameraInObjectPos - center;
//相当于把y轴往下压 如果_VerticalBillboarding=0 就代表我们把y轴压到了xz平面上 如果是1 就是正常的全方向的广告牌
normalDir.y *= _VerticalBillboarding;
//单位化Z轴的向量
normalDir = normalize(normalDir);
//模型空间下的Y轴正方向 作为它的oldUp
//为了避免Z轴和010重合,因为重合后叉乘 会得到零向量
float3 upDir = normalDir.y > 0.999 ? float3(0,0,1) : float3(0,1,0);
//利用叉乘计算X轴的方向
float3 rightDir = normalize(cross(upDir, normalDir));
//计算我们的Y轴 也就是newUp
upDir = normalize(cross(normalDir, rightDir));
//得到顶点相对于新坐标系中心点的偏移量
float3 centerOffset = v.vertex.xyz - center;
//利用三个轴向进行最终的顶点计算
float3 newVertexPos = center + rightDir * centerOffset.x + upDir * centerOffset.y + normalDir * centerOffset.z;
//把新的顶点转换到裁剪空间中
o.vertex = UnityObjectToClipPos(float4(newVertexPos, 1));
//计算UV坐标
o.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw;

return o;
}

fixed4 frag (v2f i) : SV_Target
{
//对纹理采样
float4 color = tex2D(_MainTex, i.uv);
color.rgb *= _Color.rgb;
return color;
}
ENDCG
}
}
}

三、注意事项

1.批处理相关

1.)为什么批处理会影响顶点动画


Unity中默认有静态批处理和动态批处理

批处理主要作用:
合并多个对象,将他们作为一个DrawCall进行处理

之所以批处理会影响顶点动画
是因为
不同对象拥有不同的变换矩阵(位置、渲染、缩放)
而批处理之后
他们的变换矩阵会统一进行处理

举例:


物体A:位于世界空间位置(0,0,0),无旋转
物体B:位于世界空间位置(5,0,0),无旋转
他们是两个独立的对象,拥有不同的变换矩阵

  • 不进行批处理时
    每个对象的变换矩阵会单独传递给Shader,顶点的模型空间会根据各自的变换进行正确计算
  • 进行批处理时:
    启用批处理后,Unity会将对象A和对象B合并为一个DrawCall,并使用一个统一的变换矩阵
    比如在静态批处理中,Unity会将对象A和对象B的顶点合并为一个网格,并使用统一的变换进行渲染
    批处理后顶点的位置是混合的,Shader中无法区分不同对象的模型空间位置
    可能带来的问题:
    1. 顶点动画失效;
      假设你希望顶点在顶点模型空间的x方向上进行sin波动动画
      如果对象A和对象B的模型空间位置进行混合,波动的动画会变得不可预测
    2. 变换混淆
      对象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
16
void 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. uv通道 和上面方法类似,只是将相关信息存储到uv通道中,一般存储两个值时使用

2.阴影相关


由于投射阴影的逻辑较为通用
一般直接通过 FallBack "Specular"等直接使用内置的Pass 调用 Unity 中默认 Shader 中的相关代码即可 但是这种投射的阴影,对于顶点动画来说是不正确的 因为默认的Pass当中并不会使用新的顶点来投射,而是使用原来的顶点进行计算阴影

所以这种情况我们通过自己写一个阴影投射的Pass,来实现正确的效果

阴影投射的方法 主要是三个关键点 - 一个编译指令 #pragma multi_compile_shadowcaster - 一个内置文件 - 三个关键宏 这三个宏存储于#include "UnityCG.cginc"

  1. V2F_SHADOW_CASTER
    顶点到片元着色器阴影投射结构体数据宏
    这个宏定义了一些标准的成员变量
    这些变量用于在阴影投射路径中传递顶点数据到片元着色器
    主要在结构体中使用
  2. TRANSFER_SHADOW_CASTER_NORMALOFFSET
    转移阴影投射器法线偏移宏
    用于在顶点着色器中计算和传递阴影投射所需的变量
    主要做了:
    1. 将对象空间的顶点位置转换到裁剪空间中
    2. 考虑法线偏移,以减轻阴影失真问题,尤其指在处理子阴影时
    3. 传递顶点的投影空间位置,用于后续的阴影计算 主要在顶点着色器中使用
  3. 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
82
Shader "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
#pragma vertex vert
#pragma fragment frag
//该编译指令是告诉Unity编译器生成多个着色器变体
//用于支持不同类型的阴影(SM,SSM等)
//可以确保着色器能够在所有可能的阴影投射模式下正确渲染
#pragma multi_compile_shadowcaster

//这个内置文件包含了关键的阴影计算相关的宏
#include "UnityCG.cginc"

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
}
}
}

评论