第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)); 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();
}