吹泡泡
今天来实现一个吹泡泡的案例,看一下效果
1. 引入 Three.js 和基础设置
首先,我们需要引入 Three.js 库,并进行一些基本的设置,比如创建场景、摄像机、渲染器以及控制器。
var scene, camera, renderer, cameraCtrl;
var whw, whh;
var nbObjects = 800; // 总共创建的对象数量
function init() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000);
cameraCtrl = new THREE.OrbitControls(camera);
cameraCtrl.autoRotate = true; // 自动旋转控制器
cameraCtrl.autoRotateSpeed = 5; // 自动旋转速度
renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement); // 将渲染器的DOM元素添加到页面中
initScene();
window.addEventListener("resize", onWindowResize, false); // 监听窗口大小变化
animate(); // 启动动画
}
2. 创建场景与Sprite
对象
我们将Sprite
图像用作对象的纹理,这样可以让对象看起来像是轻盈漂浮的泡泡。
在这里,使用 THREE.SpriteMaterial
创建Sprite
材质,并为每个Sprite
对象设置颜色和纹理。
var spriteMap = new THREE.Texture();
var bubble = new Image();
bubble.src = "data:image/png;base64,..."; // 图片的Base64编码
bubble.onload = function () {
spriteMap.image = bubble;
spriteMap.needsUpdate = true; // 确保图片载入完成后更新纹理
};
function initScene() {
scene.background = new THREE.Color(0x000000); // 设置场景背景为黑色
camera.position.set(0, 20, 30); // 设置摄像机位置
var objects = [];
for (var i = 0; i < nbObjects; i++) {
var object = new Truc(); // 创建 Truc 对象
objects.push(object);
scene.add(object.sprite); // 将`Sprite`对象添加到场景中
}
}
3. 创建 Truc 类与对象的初始化
Truc
类是Sprite
对象的封装,负责初始化Sprite
、设置其随机位置、缩放以及动画效果。每个Sprite
在创建时会有一个随机的初始位置,并且会在一定的时间内进行位置和透明度的动画。
3.1 Truc
构造函数
function Truc() {
this.init(); // 第一步:初始化精灵(Material + Sprite)
this.shuffle(); // 第二步:设置精灵的随机动画(位置、缩放、透明度)
}
- 创建
Truc
类的对象时,构造函数会自动执行两个核心方法:this.init()
:负责材质和精灵的基础设置。this.shuffle()
:设置精灵的随机动画逻辑,让对象产生动态的漂浮效果。
3.2 init
方法
Truc.prototype.init = function () {
this.material = new THREE.SpriteMaterial({
color: randomColor({ luminosity: "light" }), // 随机颜色
map: spriteMap, // 精灵纹理贴图
transparent: true, // 启用透明
opacity: 1, // 初始透明度
depthTest: false, // 禁用深度测试
depthWrite: false, // 禁用深度写入
blending: THREE.AdditiveBlending, // 设置混合模式为叠加
});
this.sprite = new THREE.Sprite(this.material); // 使用材质创建精灵对象
};
this.material
:- 使用
THREE.SpriteMaterial
定义精灵的材质,包含颜色、纹理、透明度及混合模式等。 - 这里的
randomColor({ luminosity: "light" })
用来生成浅色系随机颜色,让每个精灵看上去颜色不同。 map: spriteMap
即对应之前加载的纹理贴图,例如一个小的圆形泡泡或其他图案。blending: THREE.AdditiveBlending
可以实现颜色叠加的炫光效果。
- 使用
this.sprite
:- 调用
new THREE.Sprite(this.material)
创建一个基于该材质的 2D 精灵对象,能够在 Three.js 场景中使用。
- 调用
3.3 shuffle
方法
Truc.prototype.shuffle = function () {
this.scale1 = 0.1;
this.scale2 = 2 + rnd(3); // 随机的目标缩放值
this.sprite.scale.set(this.scale1, this.scale1, 1);
var rndv = getRandomVec3(); // 获取随机位置(方向随机、半径随机)
this.sprite.position.set(rndv.x, rndv.y, rndv.z).multiplyScalar(50);
this.sprite.position.y -= 25;
// 1秒内由 scale1 缩放到 scale2
TweenMax.to(this.sprite.scale, 1, {
x: this.scale2,
y: this.scale2,
ease: Power2.easeIn,
});
// 精灵在 scale2 的时长内(this.scale2秒),向上漂浮 100
TweenMax.to(this.sprite.position, this.scale2, {
y: this.sprite.position.y + 100,
ease: Power2.easeIn,
});
// 精灵的横向(x, z)位置随机摆动
TweenMax.to(this.sprite.position, this.scale2, {
x: this.sprite.position.x + rnd(10, true),
z: this.sprite.position.z + rnd(10, true),
ease: Linear.ease,
repeat: Math.floor(this.scale2 / 1), // 重复次数
yoyo: true, // 来回摆动
});
// 最后 1 秒内透明度由 1 减至 0
TweenMax.to(this.material, 1, {
opacity: 0,
delay: this.scale2 - 1, // 在动画快结束时开始变透明
ease: Power2.easeIn,
onCompleteParams: [this], // 将当前对象传给回调函数
onComplete: function (o) {
o.shuffle(); // 动画完成后重新开始,让精灵循环出现
},
});
};
this.scale1
和this.scale2
:scale1
定义初始大小(0.1),scale2
定义最终放大的倍数(2 ~ 5 之间的随机值)。
- 初始化位置:
getRandomVec3()
返回一个随机方向的x, y, z
,通过multiplyScalar(50)
增大分布范围。this.sprite.position.y -= 25
让精灵从较低的位置开始,以模拟“从下往上升起”的效果。
TweenMax
动画:- 缩放动画:1 秒内从缩放值 0.1 变为
scale2
。 - 垂直移动动画:在
scale2
秒内向上移动 100 个单位。 - 左右摆动动画:同样在
scale2
秒内对精灵的位置进行x, z
方向的来回摆动 (repeat + yoyo
)。 - 透明度渐隐:动画临近结束时,开始让精灵在 1 秒内逐渐透明,最终为 0。完成后再次调用
shuffle()
方法,形成循环。
- 缩放动画:1 秒内从缩放值 0.1 变为
4. 动画与随机位置
Sprite
对象的运动是通过 TweenMax
来实现的。每个Sprite
对象的初始位置、缩放大小以及移动路径都是随机的,以模拟自然效果。shuffle()
方法通过 TweenMax 来控制Sprite
的动画,保证了Sprite
在场景中的漂浮效果。
function getRandomVec3() {
const u = Math.random();
const v = Math.random();
const theta = u * 2.0 * Math.PI;
const phi = Math.acos(2.0 * v - 1.0);
const r = Math.cbrt(Math.random());
const x = r * Math.sin(phi) * Math.cos(theta);
const y = r * Math.sin(phi) * Math.sin(theta);
const z = r * Math.cos(phi);
return { x, y, z };
}
5. 窗口大小变化处理
当窗口大小发生变化时,我们需要重新计算摄像机的视角和渲染器的大小。这可以通过监听 resize
事件来实现。
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight; // 更新摄像机的宽高比
camera.updateProjectionMatrix(); // 更新投影矩阵
renderer.setSize(window.innerWidth, window.innerHeight); // 更新渲染器大小
}
6. 启动动画
最后,使用 requestAnimationFrame
创建一个循环动画,不断渲染场景,并更新摄像机的控制器。
function animate() {
requestAnimationFrame(animate);
cameraCtrl.update(); // 更新摄像机控制器
renderer.render(scene, camera); // 渲染场景
}
init(); // 启动初始化函数