Three.js案例
吹泡泡

吹泡泡

今天来实现一个吹泡泡的案例,看一下效果

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.scale1this.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() 方法,形成循环。

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(); // 启动初始化函数

代码

https://codepen.io/soju22/pen/aXqVQQ (opens in a new tab)