第10.4章:3D顶点动画着色器

顶点动画着色器通过在GPU上直接操作顶点位置来创建复杂的形变和动画效果,是实现高性能动态特效的核心技术。本章将深入探讨各种顶点动画技术的实现。

🎯 学习目标

  • 理解顶点动画的基本原理
  • 掌握波浪、爆炸、生长等动画效果
  • 学会使用噪声和数学函数控制顶点
  • 理解GPU顶点处理的性能优化

💡 顶点动画原理

顶点变换基础

顶点动画通过修改顶点位置实现形变:

1
2
3
4
// 基础顶点变换
vec3 originalPos = a_position;
vec3 animatedPos = originalPos + displacement;
vec4 worldPos = cc_matWorld * vec4(animatedPos, 1.0);

动画控制方式

1
2
3
时间输入 → 数学函数 → 位移计算 → 顶点变换
↓ ↓ ↓ ↓
time sin/cos offset vertex

🔧 基础顶点动画

1. 波浪形变动画

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
CCEffect %{
techniques:
- name: wave-displacement
passes:
- vert: wave-vs:vert
frag: wave-fs:frag
properties: &props
mainTexture: { value: white }
waveAmplitude: { value: 0.5, range: [0.0, 3.0] }
waveFrequency: { value: 2.0, range: [0.1, 10.0] }
waveSpeed: { value: 3.0, range: [0.0, 10.0] }
waveDirection: { value: [1.0, 0.0, 0.0], editor: { type: vec3 } }
secondaryWave: { value: 0.3, range: [0.0, 1.0] }
verticalOffset: { value: 1.0, range: [0.0, 3.0] }
noiseScale: { value: 1.0, range: [0.1, 5.0] }
baseColor: { value: [1.0, 1.0, 1.0, 1.0], editor: { type: color } }
}%

CCProgram wave-vs %{
precision highp float;
#include <cc-global>
#include <cc-local>

in vec3 a_position;
in vec3 a_normal;
in vec2 a_texCoord;

out vec3 v_worldPos;
out vec3 v_worldNormal;
out vec2 v_uv;
out float v_waveHeight;

uniform float waveAmplitude;
uniform float waveFrequency;
uniform float waveSpeed;
uniform vec3 waveDirection;
uniform float secondaryWave;
uniform float verticalOffset;
uniform float noiseScale;

// 简单噪声函数
float noise(vec3 pos) {
return fract(sin(dot(pos, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
}

vec4 vert() {
vec3 worldPos = (cc_matWorld * vec4(a_position, 1)).xyz;

// 主波浪计算
float wavePhase = dot(a_position, normalize(waveDirection)) * waveFrequency;
float mainWave = sin(wavePhase + cc_time.x * waveSpeed) * waveAmplitude;

// 次要波浪(垂直方向)
float secondWave = sin(a_position.y * waveFrequency * 2.0 + cc_time.x * waveSpeed * 1.5)
* waveAmplitude * secondaryWave;

// 噪声扰动
vec3 noisePos = a_position * noiseScale + cc_time.x * 0.5;
float noiseOffset = (noise(noisePos) - 0.5) * waveAmplitude * 0.3;

// 合成位移
float totalDisplacement = mainWave + secondWave + noiseOffset;

// 垂直偏移
vec3 animatedPos = a_position;
animatedPos.y += totalDisplacement * verticalOffset;

// 法线计算(简化)
vec3 tangent = normalize(waveDirection);
float gradient = cos(wavePhase + cc_time.x * waveSpeed) * waveAmplitude * waveFrequency;
vec3 animatedNormal = normalize(a_normal + tangent * gradient * 0.1);

v_worldPos = (cc_matWorld * vec4(animatedPos, 1)).xyz;
v_worldNormal = normalize((cc_matWorldIT * vec4(animatedNormal, 0)).xyz);
v_uv = a_texCoord;
v_waveHeight = totalDisplacement;

return cc_matViewProj * cc_matWorld * vec4(animatedPos, 1);
}
}%

CCProgram wave-fs %{
precision highp float;
#include <cc-global>
#include <cc-environment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;

in vec3 v_worldPos;
in vec3 v_worldNormal;
in vec2 v_uv;
in float v_waveHeight;

vec4 frag() {
vec3 normal = normalize(v_worldNormal);

// 基础纹理
vec4 albedo = texture(mainTexture, v_uv) * baseColor;

// 根据波浪高度调整颜色
float heightFactor = v_waveHeight * 0.5 + 0.5;
vec3 waveColor = mix(vec3(0.2, 0.4, 0.8), vec3(0.8, 0.9, 1.0), heightFactor);

// 简单光照
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float NdotL = max(0.0, dot(normal, lightDir));

vec3 finalColor = albedo.rgb * waveColor * (0.3 + 0.7 * NdotL);

return vec4(finalColor, albedo.a);
}
}%

2. 爆炸扩散动画

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
CCEffect %{
techniques:
- name: explosion-displacement
passes:
- vert: explosion-vs:vert
frag: explosion-fs:frag
properties: &props
mainTexture: { value: white }
explosionCenter: { value: [0.0, 0.0, 0.0], editor: { type: vec3 } }
explosionRadius: { value: 2.0, range: [0.1, 10.0] }
explosionForce: { value: 3.0, range: [0.0, 10.0] }
explosionTime: { value: 0.0, range: [0.0, 5.0] }
fragmentSize: { value: 0.1, range: [0.01, 1.0] }
randomSeed: { value: 123.456, range: [0.0, 1000.0] }
gravityEffect: { value: 1.0, range: [0.0, 3.0] }
rotationSpeed: { value: 5.0, range: [0.0, 20.0] }
baseColor: { value: [1.0, 1.0, 1.0, 1.0], editor: { type: color } }
}%

CCProgram explosion-vs %{
precision highp float;
#include <cc-global>
#include <cc-local>

in vec3 a_position;
in vec3 a_normal;
in vec2 a_texCoord;

out vec3 v_worldPos;
out vec3 v_worldNormal;
out vec2 v_uv;
out float v_explosionFactor;

uniform vec3 explosionCenter;
uniform float explosionRadius;
uniform float explosionForce;
uniform float explosionTime;
uniform float fragmentSize;
uniform float randomSeed;
uniform float gravityEffect;
uniform float rotationSpeed;

// 随机函数
float random(vec3 seed) {
return fract(sin(dot(seed, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
}

vec3 randomDirection(vec3 seed) {
float theta = random(seed) * 6.28318;
float phi = random(seed + vec3(1.0)) * 3.14159;
return vec3(sin(phi) * cos(theta), cos(phi), sin(phi) * sin(theta));
}

vec4 vert() {
vec3 worldPos = (cc_matWorld * vec4(a_position, 1)).xyz;

// 距离爆炸中心的距离
vec3 toCenter = worldPos - explosionCenter;
float distanceToCenter = length(toCenter);

// 爆炸影响因子
float explosionInfluence = 1.0 - smoothstep(0.0, explosionRadius, distanceToCenter);

// 碎片随机种子
vec3 fragmentSeed = a_position + vec3(randomSeed);

// 爆炸方向(从中心向外 + 随机偏移)
vec3 explosionDir = normalize(toCenter + randomDirection(fragmentSeed) * 0.5);

// 爆炸位移
float explosionProgress = min(explosionTime / 2.0, 1.0);
vec3 explosionOffset = explosionDir * explosionForce * explosionInfluence * explosionProgress;

// 重力效果
explosionOffset.y -= gravityEffect * explosionProgress * explosionProgress;

// 碎片大小变化
float fragmentScale = 1.0 - explosionInfluence * fragmentSize * explosionProgress;

// 旋转效果
float rotationAngle = explosionProgress * rotationSpeed * random(fragmentSeed + vec3(2.0));
vec3 rotationAxis = randomDirection(fragmentSeed + vec3(3.0));

// 简化的旋转(仅绕Y轴)
float cosAngle = cos(rotationAngle);
float sinAngle = sin(rotationAngle);
vec3 rotatedPos = a_position;
rotatedPos.x = a_position.x * cosAngle - a_position.z * sinAngle;
rotatedPos.z = a_position.x * sinAngle + a_position.z * cosAngle;

// 最终位置计算
vec3 animatedPos = rotatedPos * fragmentScale + explosionOffset;

v_worldPos = (cc_matWorld * vec4(animatedPos, 1)).xyz;
v_worldNormal = normalize((cc_matWorldIT * vec4(a_normal, 0)).xyz);
v_uv = a_texCoord;
v_explosionFactor = explosionInfluence * explosionProgress;

return cc_matViewProj * cc_matWorld * vec4(animatedPos, 1);
}
}%

CCProgram explosion-fs %{
precision highp float;
#include <cc-global>
#include <cc-environment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;

in vec3 v_worldPos;
in vec3 v_worldNormal;
in vec2 v_uv;
in float v_explosionFactor;

vec4 frag() {
// 基础纹理
vec4 albedo = texture(mainTexture, v_uv) * baseColor;

// 爆炸发光效果
vec3 explosionColor = mix(vec3(1.0), vec3(1.0, 0.3, 0.1), v_explosionFactor);

// 透明度变化
float explosionAlpha = 1.0 - v_explosionFactor * 0.8;

vec3 finalColor = albedo.rgb * explosionColor;

return vec4(finalColor, albedo.a * explosionAlpha);
}
}%

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
CCEffect %{
techniques:
- name: growth-animation
passes:
- vert: growth-vs:vert
frag: growth-fs:frag
properties: &props
mainTexture: { value: white }
growthProgress: { value: 0.5, range: [0.0, 1.0] }
growthOrigin: { value: [0.0, -1.0, 0.0], editor: { type: vec3 } }
growthHeight: { value: 2.0, range: [0.1, 5.0] }
twistAmount: { value: 0.0, range: [0.0, 6.28] }
scaleVariation: { value: 0.2, range: [0.0, 1.0] }
noiseScale: { value: 1.0, range: [0.1, 5.0] }
bendStrength: { value: 0.5, range: [0.0, 2.0] }
windEffect: { value: 0.3, range: [0.0, 1.0] }
baseColor: { value: [0.2, 0.8, 0.3, 1.0], editor: { type: color } }
}%

CCProgram growth-vs %{
precision highp float;
#include <cc-global>
#include <cc-local>

in vec3 a_position;
in vec3 a_normal;
in vec2 a_texCoord;

out vec3 v_worldPos;
out vec3 v_worldNormal;
out vec2 v_uv;
out float v_growthFactor;

uniform float growthProgress;
uniform vec3 growthOrigin;
uniform float growthHeight;
uniform float twistAmount;
uniform float scaleVariation;
uniform float noiseScale;
uniform float bendStrength;
uniform float windEffect;

// 噪声函数
float noise(vec3 pos) {
return fract(sin(dot(pos, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
}

vec4 vert() {
vec3 pos = a_position;

// 计算相对于生长原点的高度
float relativeHeight = (pos.y - growthOrigin.y) / growthHeight;
relativeHeight = clamp(relativeHeight, 0.0, 1.0);

// 生长遮罩
float growthMask = step(relativeHeight, growthProgress);
float growthFade = 1.0 - smoothstep(growthProgress - 0.1, growthProgress, relativeHeight);

// 扭转效果
float twistAngle = twistAmount * relativeHeight * growthProgress;
float cosAngle = cos(twistAngle);
float sinAngle = sin(twistAngle);

vec3 twistedPos = pos;
twistedPos.x = pos.x * cosAngle - pos.z * sinAngle;
twistedPos.z = pos.x * sinAngle + pos.z * cosAngle;

// 缩放变化
vec3 noisePos = pos * noiseScale;
float scaleNoise = noise(noisePos) * scaleVariation;
float scaleModifier = 1.0 + scaleNoise * relativeHeight;

// 弯曲效果
float bendOffset = sin(relativeHeight * 3.14159) * bendStrength * growthProgress;
twistedPos.x += bendOffset * relativeHeight;

// 风效果
float windSway = sin(cc_time.x * 2.0 + pos.x) * windEffect * relativeHeight * growthProgress;
twistedPos.x += windSway;
twistedPos.z += windSway * 0.5;

// 应用生长效果
vec3 animatedPos = mix(growthOrigin, twistedPos, growthMask * growthFade) * scaleModifier;

// 法线调整(简化)
vec3 animatedNormal = a_normal;
if (twistAmount > 0.0) {
animatedNormal.x = a_normal.x * cosAngle - a_normal.z * sinAngle;
animatedNormal.z = a_normal.x * sinAngle + a_normal.z * cosAngle;
}

v_worldPos = (cc_matWorld * vec4(animatedPos, 1)).xyz;
v_worldNormal = normalize((cc_matWorldIT * vec4(animatedNormal, 0)).xyz);
v_uv = a_texCoord;
v_growthFactor = relativeHeight * growthProgress;

return cc_matViewProj * cc_matWorld * vec4(animatedPos, 1);
}
}%

CCProgram growth-fs %{
precision highp float;
#include <cc-global>
#include <cc-environment>

uniform sampler2D mainTexture;
uniform vec4 baseColor;

in vec3 v_worldPos;
in vec3 v_worldNormal;
in vec2 v_uv;
in float v_growthFactor;

vec4 frag() {
vec3 normal = normalize(v_worldNormal);

// 基础纹理
vec4 albedo = texture(mainTexture, v_uv) * baseColor;

// 生长颜色渐变
vec3 youngColor = vec3(0.8, 1.0, 0.3); // 嫩绿色
vec3 matureColor = vec3(0.2, 0.7, 0.1); // 深绿色
vec3 growthColor = mix(youngColor, matureColor, v_growthFactor);

// 简单光照
vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
float NdotL = max(0.0, dot(normal, lightDir));

vec3 finalColor = albedo.rgb * growthColor * (0.4 + 0.6 * NdotL);

return vec4(finalColor, albedo.a);
}
}%

🎨 高级顶点动画

1. 液体模拟动画

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
CCEffect %{
techniques:
- name: liquid-simulation
passes:
- vert: liquid-vs:vert
frag: liquid-fs:frag
properties: &props
mainTexture: { value: white }
liquidDensity: { value: 1.0, range: [0.1, 5.0] }
viscosity: { value: 0.5, range: [0.0, 2.0] }
surfaceTension: { value: 0.8, range: [0.0, 2.0] }
flowDirection: { value: [0.0, -1.0, 0.0], editor: { type: vec3 } }
turbulence: { value: 0.3, range: [0.0, 1.0] }
waveHeight: { value: 0.2, range: [0.0, 1.0] }
flowSpeed: { value: 1.0, range: [0.0, 5.0] }
liquidColor: { value: [0.2, 0.6, 1.0, 0.8], editor: { type: color } }
}%

CCProgram liquid-vs %{
precision highp float;
#include <cc-global>
#include <cc-local>

in vec3 a_position;
in vec3 a_normal;
in vec2 a_texCoord;

out vec3 v_worldPos;
out vec3 v_worldNormal;
out vec2 v_uv;
out float v_liquidHeight;

uniform float liquidDensity;
uniform float viscosity;
uniform float surfaceTension;
uniform vec3 flowDirection;
uniform float turbulence;
uniform float waveHeight;
uniform float flowSpeed;

// 多层噪声
float noise(vec3 pos) {
return fract(sin(dot(pos, vec3(12.9898, 78.233, 45.164))) * 43758.5453);
}

float fbm(vec3 pos) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;

for (int i = 0; i < 4; i++) {
value += noise(pos * frequency) * amplitude;
amplitude *= 0.5;
frequency *= 2.0;
}

return value;
}

vec4 vert() {
vec3 pos = a_position;

// 时间偏移
float time = cc_time.x * flowSpeed;

// 主流动
vec3 flowOffset = flowDirection * time * viscosity;

// 表面波动
float wavePhase = dot(pos.xz, vec2(1.0, 0.5)) * 3.0 + time;
float wave1 = sin(wavePhase) * waveHeight;
float wave2 = sin(wavePhase * 1.7 + 1.3) * waveHeight * 0.5;

// 湍流
vec3 turbulencePos = pos + flowOffset + vec3(time * 0.3);
float turbulenceNoise = fbm(turbulencePos * liquidDensity) * turbulence;

// 表面张力效果
float surface = surfaceTension * sin(pos.x * 5.0 + time) * cos(pos.z * 5.0 + time * 1.3) * 0.1;

// 合成液体位移
vec3 liquidPos = pos;
liquidPos.y += wave1 + wave2 + turbulenceNoise + surface;
liquidPos += flowOffset * 0.1;

// 法线扰动计算
float dx = (sin((pos.x + 0.01) * 3.0 + time) - sin((pos.x - 0.01) * 3.0 + time)) / 0.02;
float dz = (sin((pos.z + 0.01) * 3.0 + time) - sin((pos.z - 0.01) * 3.0 + time)) / 0.02;
vec3 liquidNormal = normalize(vec3(-dx, 1.0, -dz));

v_worldPos = (cc_matWorld * vec4(liquidPos, 1)).xyz;
v_worldNormal = normalize((cc_matWorldIT * vec4(liquidNormal, 0)).xyz);
v_uv = a_texCoord + flowOffset.xz * 0.1;
v_liquidHeight = liquidPos.y - pos.y;

return cc_matViewProj * cc_matWorld * vec4(liquidPos, 1);
}
}%

CCProgram liquid-fs %{
precision highp float;
#include <cc-global>
#include <cc-environment>

uniform sampler2D mainTexture;
uniform vec4 liquidColor;

in vec3 v_worldPos;
in vec3 v_worldNormal;
in vec2 v_uv;
in float v_liquidHeight;

vec4 frag() {
vec3 normal = normalize(v_worldNormal);
vec3 viewDir = normalize(cc_cameraPos.xyz - v_worldPos);

// 基础纹理
vec4 albedo = texture(mainTexture, v_uv);

// 菲涅尔反射(液体特性)
float fresnel = pow(1.0 - max(0.0, dot(normal, viewDir)), 3.0);

// 液体颜色混合
vec3 liquidFinal = mix(liquidColor.rgb, albedo.rgb, 0.3);
liquidFinal += vec3(0.8, 0.9, 1.0) * fresnel * 0.5;

// 根据高度变化调整透明度和颜色
float heightFactor = clamp(v_liquidHeight * 2.0 + 0.5, 0.0, 1.0);
liquidFinal *= heightFactor;

float finalAlpha = liquidColor.a * (0.7 + fresnel * 0.3) * heightFactor;

return vec4(liquidFinal, finalAlpha);
}
}%

🔧 TypeScript控制系统

顶点动画控制器

import { Component, Material, Vec3, _decorator } from 'cc';

const { ccclass, property, menu } = _decorator;

enum AnimationType {
    Wave = 'wave-displacement',
    Explosion = 'explosion-displacement',
    Growth = 'growth-animation',
    Liquid = 'liquid-simulation'
}

@ccclass('VertexAnimationController')
@menu('Custom/VertexAnimationController')
export class VertexAnimationController extends Component {
    
    @property({ range: [0.0, 3.0], displayName: '动画强度' })
    public animationIntensity: number = 1.0;
    
    @property({ range: [0.0, 10.0], displayName: '动画速度' })
    public animationSpeed: number = 1.0;
    
    @property({ type: Vec3, displayName: '动画方向' })
    public animationDirection: Vec3 = new Vec3(1, 0, 0);
    
    @property({ displayName: '动画类型' })
    public animationType: AnimationType = AnimationType.Wave;
    
    // 波浪动画参数
    @property({ range: [0.0, 3.0], displayName: '波浪幅度' })
    public waveAmplitude: number = 0.5;
    
    @property({ range: [0.1, 10.0], displayName: '波浪频率' })
    public waveFrequency: number = 2.0;
    
    // 爆炸动画参数
    @property({ type: Vec3, displayName: '爆炸中心' })
    public explosionCenter: Vec3 = new Vec3(0, 0, 0);
    
    @property({ range: [0.1, 10.0], displayName: '爆炸半径' })
    public explosionRadius: number = 2.0;
    
    @property({ range: [0.0, 10.0], displayName: '爆炸力度' })
    public explosionForce: number = 3.0;
    
    @property({ range: [0.0, 5.0], displayName: '爆炸时间' })
    public explosionTime: number = 0.0;
    
    // 生长动画参数
    @property({ range: [0.0, 1.0], displayName: '生长进度' })
    public growthProgress: number = 0.5;
    
    @property({ type: Vec3, displayName: '生长原点' })
    public growthOrigin: Vec3 = new Vec3(0, -1, 0);
    
    @property({ range: [0.1, 5.0], displayName: '生长高度' })
    public growthHeight: number = 2.0;
    
    // 液体模拟参数
    @property({ range: [0.1, 5.0], displayName: '液体密度' })
    public liquidDensity: number = 1.0;
    
    @property({ range: [0.0, 2.0], displayName: '粘度' })
    public viscosity: number = 0.5;
    
    @property({ range: [0.0, 1.0], displayName: '湍流强度' })
    public turbulence: number = 0.3;
    
    private _material: Material | null = null;
    private _isAnimating: boolean = false;
    private _animationTimer: number = 0;
    
    onLoad() {
        const renderer = this.getComponent('cc.MeshRenderer');
        if (renderer && renderer.material) {
            this._material = renderer.material;
            this.updateMaterial();
        }
    }
    
    update(deltaTime: number) {
        if (this._isAnimating) {
            this._animationTimer += deltaTime;
            this.updateMaterial();
        }