Three.js + GSAP 实现程序化植物生长与绽放动画

当花瓣在屏幕上缓缓展开,当叶片从芽苞中抽出——这些原本属于自然的瞬间,正在被 WebGL 技术重新定义。本文将深入探讨如何在网页中实现程序化的植物生长与绽放动画。

引言:为什么需要在网页中加入 3D 植物动画

在用户注意力日趋分散的今天,网页需要在第一秒内抓住访问者的眼球。传统的静态 Banner 和渐变背景已经难以产生视觉冲击力,而 3D 植物动画提供了一种全新的可能性。

应用场景主要包括:

  • 首页 Banner:作为品牌展示的第一视觉焦点,动态植物能传达生长、生命力等品牌联想
  • 文章配图:技术文章中使用程序化生成的植物元素,既美观又具有独特性
  • 背景粒子:飘落的花瓣、飘散的种子,营造沉浸式阅读氛围

相比传统的 CSS 动画或 Canvas 2D,Three.js 带来的 3D 渲染能够提供更真实的 depth 感和空间关系,让动画具有「呼吸感」——这是二维动画难以企及的维度。

技术选型:为什么是这三件套

Three.js:WebGL 渲染的核心

选择 Three.js 而非 Canvas 2D,核心原因在于性能和表现力的平衡

Canvas 2D 在处理大量粒子时会遇到性能瓶颈,而 WebGL 通过 GPU 加速可以轻松处理数万个顶点的渲染。Three.js 提供了完善的场景图管理、材质系统和相机控制,让我们能专注于植物形态的数学建模,而非底层渲染细节。

1
2
3
4
5
6
// 基础场景搭建
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

GSAP:动画控制的首选

GSAP(GreenSock Animation Platform)是目前业界最成熟的 Web 动画库。相比 CSS Animation,GSAP 提供了:

  • 时间轴控制:可以精确编排多阶段动画的时序
  • 插值函数:丰富的 easing 函数,满足不同运动曲线需求
  • 性能优化:自动处理 RAF(RequestAnimationFrame)循环

至于为什么不用 Matter.js——这个物理引擎虽然强大,但对于植物生长这种确定性动画来说过于重量级。我们的动画需要的是「可控的美感」,而非物理真实。

L-System:植物生成的数学基础

L-System(Lindenmayer System)是一种基于重写规则的形式语言,最初用于模拟植物形态。其核心思想很简单:

1
起始符 → 迭代规则 → 最终形态

例如,一个简单的植物分枝规则:

  • 起始:X
  • 规则:X → F[+X][-X]FX(F=向前,+/-=旋转,[]=保存/恢复状态)

通过调整迭代次数和角度参数,我们可以生成从简单到复杂的各种植物形态。这种程序化生成方式的优势在于:一套代码,无限形态

在线 Demo

为了让大家更直观地感受植物生长的完整过程,我编写了一个交互式 Demo:

🌐 在线演示地址: 植物生长动画 Demo

Demo 包含五个生长阶段的交互按钮:

  • 🌱 种子期:仅显示地面
  • 🌿 发芽期:抽出第一片叶子
  • 🌳 生长期:茎秆延伸,叶片展开
  • 🌸 绽放期:花瓣逐层展开
  • 🍃 飘散期:花瓣凋落飘散

💡 提示:Demo 会自动环绕旋转,可拖动查看不同角度。在”飘散期”可以观察到粒子系统的物理模拟效果。


核心实现:三个关键技术点

1. 状态机控制绽放阶段

植物生长是一个多阶段过程,我们需要用状态机来管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PlantStateMachine {
constructor() {
this.states = {
SEED: 'seed', // 种子期:仅显示地面小土包
SPROUT: 'sprout', // 发芽期:抽出第一片叶子
GROWTH: 'growth', // 生长期:茎秆延伸,叶片展开
BLOOM: 'bloom', // 绽放期:花瓣逐层展开
SCATTER: 'scatter' // 飘散期:花瓣凋落、种子飞散
};
this.currentState = this.states.SEED;
}

transition(newState) {
// 状态转换前的清理工作
this.currentState = newState;
}
}

每个状态对应不同的动画序列,通过 GSAP Timeline 串联:

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
function playBloomAnimation() {
const tl = gsap.timeline();

// 第一阶段:花苞膨胀
tl.to(flowerBud.scale, {
x: 1.5, y: 1.5, z: 1.5,
duration: 0.8,
ease: 'back.out(1.7)'
});

// 第二阶段:外层花瓣展开
tl.to(outerPetals.rotation, {
y: Math.PI * 0.4,
duration: 1.2,
ease: 'power2.inOut'
}, '-=0.3');

// 第三阶段:内层花瓣展开
tl.to(innerPetals.rotation, {
y: Math.PI * 0.6,
duration: 1.0,
ease: 'power2.out'
}, '-=0.8');

return tl;
}

2. 数学函数生成形态

植物的几何形态通过参数化函数生成,这里以花瓣为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createPetalGeometry() {
const shape = new THREE.Shape();

// 使用贝塞尔曲线绘制花瓣轮廓
shape.moveTo(0, 0);
shape.bezierCurveTo(0.5, 0.5, 1, 1, 0, 2); // 右侧曲线
shape.bezierCurveTo(-1, 1, -0.5, 0.5, 0, 0); // 左侧曲线

const extrudeSettings = {
steps: 2,
depth: 0.05,
bevelEnabled: true,
bevelThickness: 0.02,
bevelSize: 0.02,
bevelSegments: 3
};

return new THREE.ExtrudeGeometry(shape, extrudeSettings);
}

对于叶片,可以使用顶点着色器实现弯曲效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 简单的顶点位移模拟叶片弯曲
const leafVertexShader = `
uniform float time;
uniform float bendAmount;

void main() {
vec3 pos = position;

// 基于高度的弯曲
float bendFactor = pos.y * pos.y * bendAmount;
pos.x += sin(time * 0.5) * bendFactor * 0.1;
pos.z += cos(time * 0.3) * bendFactor * 0.1;

gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`;

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
class PetalParticle {
constructor(startPosition) {
this.position = startPosition.clone();
this.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.02,
Math.random() * 0.01 + 0.005,
(Math.random() - 0.5) * 0.02
);
this.rotationSpeed = new THREE.Vector3(
Math.random() * 0.05,
Math.random() * 0.05,
Math.random() * 0.05
);
this.life = 1.0;
}

update(deltaTime) {
// 重力
this.velocity.y -= 0.0001;

// 空气阻力
this.velocity.multiplyScalar(0.99);

// 风力扰动
this.velocity.x += Math.sin(Date.now() * 0.001 + this.position.y) * 0.0002;

// 更新位置
this.position.add(this.velocity);

// 旋转
this.mesh.rotation.add(this.rotationSpeed);

// 生命周期衰减
this.life -= deltaTime * 0.3;

return this.life > 0;
}
}

这个简化模型保留了飘落的美感,同时将性能开销降到最低。

性能优化:这是重点!

再漂亮的动画如果导致页面卡顿,都是失败的。以下是我们实践出的优化策略:

1. 懒加载:IntersectionObserver

不要在页面加载时立即初始化 3D 场景,等到元素进入视口再加载:

1
2
3
4
5
6
7
8
9
10
11
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !sceneInitialized) {
initThreeJS();
animate();
observer.disconnect();
}
});
}, { threshold: 0.1 });

observer.observe(document.querySelector('#plant-canvas'));

2. 设备检测降级

低端移动设备应该获得简化版体验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getDeviceLevel() {
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
const gpuTier = navigator.hardwareConcurrency || 4;

if (isMobile && gpuTier <= 4) return 'low';
if (isMobile) return 'medium';
return 'high';
}

const CONFIG = {
high: { particleCount: 200, shadowQuality: 'high', antialias: true },
medium: { particleCount: 100, shadowQuality: 'medium', antialias: false },
low: { particleCount: 50, shadowQuality: 'none', antialias: false }
};

3. InstancedMesh 优化

对于大量相同几何体(如花瓣、叶子),必须使用 InstancedMesh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const petalGeometry = createPetalGeometry();
const petalMaterial = new THREE.MeshStandardMaterial({
color: 0xff69b4,
side: THREE.DoubleSide
});

const petalCount = 50;
const instancedPetals = new THREE.InstancedMesh(
petalGeometry,
petalMaterial,
petalCount
);

// 批量更新矩阵而非逐个创建 Mesh
const dummy = new THREE.Object3D();
for (let i = 0; i < petalCount; i++) {
dummy.position.set(randomX, randomY, randomZ);
dummy.updateMatrix();
instancedPetals.setMatrixAt(i, dummy.matrix);
}
instancedPetals.instanceMatrix.needsUpdate = true;

4. 粒子数限制

设备层级 粒子数上限 阴影质量
高端桌面 200 High
主流手机 100 Medium
低端设备 50 None

风险与注意事项

首屏性能影响

3D 场景的初始化会阻塞主线程。解决方案:

  • 使用 requestIdleCallback 延迟初始化
  • 显示 Loading 动画而非空白
  • 考虑使用 Web Worker 进行几何计算

内存泄漏风险

Three.js 的几何体和材质需要手动管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 组件卸载时清理
function disposeScene() {
scene.traverse((object) => {
if (object.geometry) object.geometry.dispose();
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(m => m.dispose());
} else {
object.material.dispose();
}
}
});
renderer.dispose();
}

移动端兼容性

  • 限制 pixelRatio 为 2,避免高分屏过热
  • 触摸设备禁用复杂的鼠标交互
  • 考虑提供「简化模式」开关

总结

通过 Three.js + GSAP + L-System 的组合,我们可以在网页中实现可控、美观、高性能的植物生长动画。关键在于:

  1. 状态机管理多阶段动画时序
  2. 数学函数实现程序化形态生成
  3. 性能优化确保流畅用户体验

技术的价值在于服务于体验。当花瓣在屏幕上绽放时,那一瞬的自然之美,正是 WebGL 带给我们的礼物。


参考:Three.js 官方文档、GSAP 动画库、L-System 算法、Codrops: Fractals to Forests