Three.js 进阶之旅:神奇的粒子系统-迷失太空 👨‍🚀

10,272 阅读10分钟

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

摘要

粒子 particle 和 精灵 sprite 是在三维开发中经常用到的网格模型,在 Three.js 中使用 THREE.Points 可以非常容易地创建很多细小的物体,可以用来模拟星辰、雨滴、雪花、烟雾、火焰和其他有趣的效果。本文将讨论关于 Three.js 中各种创建粒子的方式和以及如何优化粒子的样式和使用粒子,最终将结合本文所讲的粒子知识,制作一个充满趣味和创意的 3D 粒子页面——迷失太空

通过阅读本文及配套对应代码,你将学到的内容包括:使用 THREE.Sprite 创建粒子集合、使用 THREE.Points 创建粒子集合、如何创建样式化的粒子、使用 dat.GUI 动态控制页面参数、使用 Canvas 样式化粒子、使用纹理贴图样式化粒子、从高级几何体创建粒子、给场景添加 FogFogExp2 雾化效果、使用 正余弦函数 给模型添加动画效果、鼠标移动动画等。

效果

本文代码中包含7个粒子效果示例,解开代码中相应的注释即可查看每种粒子效果。迷失太空是本文中最后综合应用粒子知识实现的一个创意页面,下图就是它的实现效果。整个页面类似科幻电影《地心引力》的海报,宇航员不慎迷失太空,逃逸向无边无际的宇宙深处 🌌。在屏幕上使用鼠标移动,宇航员 👨‍🚀 和 星辰 都会根据鼠标的移动产生相反方向的位移动画。

preview.gif

打开以下链接中的任意一个,在线预览效果,大屏访问效果更佳。

本专栏系列代码托管在 Github 仓库【threejs-odessey】后续所有目录也都将在此仓库中更新

🔗 代码仓库地址:git@github.com:dragonir/threejs-ode…

《地心引力》海报

gravity.png

码上掘金

实现

本文中有7个关于 Three.js 粒子效果的示例,为了方便,全都写在一个文件中,下述步骤中资源引入场景初始化都是通用的,其他步骤 0~6 都是一个个单独的示例。在代码中,只要解开每个示例的注释,即可查看对应粒子效果。

资源引入

本示例中,除了引入样式表、Three.js、镜头轨道控制器 OrbitControls、模型加载器 GLTFLoader 之外,还额外引入dat.GUI,它是可以通过在页面上添加一个可视化控制器来修改代码参数的库,方便动态查看和调试页面在各种参数下的渲染效果,具体用法在本文后续内容中有详细的描述。

import './style.css';
import * as THREE from 'three';
import * as dat from 'dat.gui';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

场景初始化

正式开发粒子效果之前,需要经过初始化渲染器、场景、相机、缩放事件监听、页面重绘动画调用等必备步骤。关于它们的具体原理本文不再赘述,需要了解的可前往本专栏前两章查看《Three.js 进阶之旅:基础入门(上)》《Three.js 进阶之旅:基础入门(下)》

// 初始化渲染器
const canvas = document.querySelector('canvas.webgl');
const renderer = new THREE.WebGLRenderer({ canvas: canvas });
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

// 初始化场景
const scene = new THREE.Scene();

// 初始化相机
const camera = new THREE.PerspectiveCamera(45, sizes.width / sizes.height, 0.1, 1000)
camera.position.z = 120
camera.lookAt(new THREE.Vector3(0, 0, 0))
scene.add(camera);

// 镜头控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 页面缩放事件监听
window.addEventListener('resize', () => {
  sizes.width = window.innerWidth;
  sizes.height = window.innerHeight;
  // 更新渲染
  renderer.setSize(sizes.width, sizes.height);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  // 更新相机
  camera.aspect = sizes.width / sizes.height;
  camera.updateProjectionMatrix();
});

// 页面重绘动画
const tick = () => {
  controls && controls.update();
  // 更新渲染器
  renderer.render(scene, camera);
  // 页面重绘时调用自身
  window.requestAnimationFrame(tick);
}
tick();

〇 使用THREE.Sprite创建粒子

Three.js 提供多种方法创建粒子,首先我们使用 THREE.Sprite 来通过如下的方式创建一个 20 x 30 的粒子系统。通过 new THREE.Sprite() 构造方法来创建粒子,给它传入唯一的参数材质,此时可选的材质类型只能是 THREE.SpriteMaterialTHREE.SpriteCanvasMaterial。创建材质时将它的 color 属性值设置成了随机色。由于THREE.Sprite 对象继承于 THREE.Object3D,它的大多数属性和方法都可以直接使用。示例中使用了 position 方法对粒子进行定位设置。还可以使用 scale 属性进行缩放、使用 translate 属性进行位移设置等。

const createParticlesBySprite = () => {
  for (let x = -15; x < 15; x++) {
    for(let y = -10; y < 10; y++) {
      let material = new THREE.SpriteMaterial({
        color: Math.random() * 0xffffff
      });
      let sprite = new THREE.Sprite(material);
      sprite.position.set(x * 4, y * 4, 0);
      scene.add(sprite);
    }
  }
}

将创建的粒子系统添加到场景中,就能看到如下图所示的粒子方块点阵。它们是一个个的彩色方块构成的网格,如果使用鼠标或触控板在场景中移动,你会发现,无论从哪个角度观察,阵列中的彩色方块看起来都没有变化,每一个粒子都是永远面向摄像机的二维平面,如果在创建粒子的时候没有指定任何属性,那么它们将会被渲染成二维的白色小方块

method_0.gif

知识点 💡 精灵材质 THREE.SpriteMaterial

THREE.SpriteMatrial 对象的一些可修改属性及其说明。

  • color:粒子的颜色。
  • map:粒子所用的纹理,可以是一组 sprite sheet
  • sizeAttenuation:如果该属性设置为 false,那么距离摄像机的远近不影响粒子的大小,默认值为 true
  • opacity:该属性设置粒子的不透明度。默认值为 1,不透明。
  • blending:该属性指定渲染粒子时所用的融合模式。
  • fog:该属性决定粒子是否受场景中雾化效果影响。默认值为 true

① 使用THREE.Points创建粒子

通过 THREE.Sprite 你可以非常容易地创建一组对象并在场景中移动它们。当你使用少量的对象时,这会很有效,但是如果需要创建大量的粒子,如果这时候还是使用 THREE.Sprite 的话,就会产生性能问题,因为每个对象需要分别由 Three.js 进行管理。

Three.js 提供了另一种方式来处理大量的粒子,就是使用 THREE.Points,通过 Three.PointsThree.js 不需要管理大量 THREE.Sprite 对象,而只需要管理 THREE.Points 实例。使用这种方法创建粒子系统时,首先要创建粒子的网格 THREE.BufferGeometry,然后创建粒子的材质 THREE.PointsMaterial。然后创建两个数组 veticsFloat32ArrayveticsColors,用来管理粒子系统中每个粒子的位置和颜色,通过 THREE.Float32BufferAttribute 将它们设置为网格属性。最后使用 THREE.Points 将创建的网格和材质变为粒子系统添加到场景中。

const createParticlesByPoints = () => {
  const geom = new THREE.BufferGeometry();
  const material = new THREE.PointsMaterial({
    size: 3,
    vertexColors: true,
    color: 0xffffff
  });
  let veticsFloat32Array = []
  let veticsColors = []
  for (let x = -15; x < 15; x++) {
    for (let y = -10; y < 10; y++) {
        veticsFloat32Array.push(x * 4, y * 4, 0);
        const randomColor = new THREE.Color(Math.random() * 0xffffff);
        veticsColors.push(randomColor.r, randomColor.g, randomColor.b);
    }
  }
  const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
  const colors = new THREE.Float32BufferAttribute(veticsColors, 3);
  geom.attributes.position = vertices;
  geom.attributes.color = colors;
  const particles = new THREE.Points(geom, material);
  scene.add(particles);
}

实现效果如下图所示,使用 THREE.Points 可以得到和使用 THREE.Sprite 相同的结果。

method_1.png

知识点 💡 点材质 THREE.PointsMaterial

上述例子中使用 THREE.PointsMaterial 来样式化粒子,它是 THREE.Points 使用的默认材质,下面列举了 THREE.PointsMaterial 中所有可设置属性及其说明。

  • color: 粒子系统中所有粒子的颜色。将 vertexColors 属性设置为 true,并且通过颜色属性指定了几何体的颜色来覆盖该属性。默认值为 0xFFFFFF
  • map: 通过这个属性可以在粒子材质,比如可以使用 canvas、贴图等。
  • size:该属性指定粒子的大小,默认值为 1
  • sizeAnnutation: 如果该属性设置为 false,那么所有的粒子都将拥有相同的尺寸,无论它们距离相机有多远。如果设置为 true,粒子的大小取决于其距离摄像机的距离的远近,默认值为true
  • vertexColors:通常 THREE.Points 中所有的粒子都拥有相同的颜色,如果该属性设置为 THREE.VertexColors,并且几何体的颜色数组也有值,那就会使用颜色数组中的值,默认值为 THREE.NoColors
  • opacity:该属性与 transparent 属性一起使用,用来设置粒子的不透明度。默认值为 1(完全不透明)。
  • transparent:如果该属性设置为 true,那么粒子在渲染时会根据 opacity 属性的值来确定其透明度,默认值为 false
  • blending:该属性指定渲染粒子时的融合模式。
  • fog:该属性决定粒子是否受场景中雾化效果影响,默认值为 true

② 创建样式化的粒子

在上个例子的基础上,我们改造一下创建粒子的方法,通过给 THREE.PointsMaterial 动态传入参数的方式来修改粒子的样式。为了能够实时修改参数并同时能够在页面上查看到参数改变之后的效果,我们可以使用 dat.GUI 库来实现这一功能。首先,通过 new dat.GUI() 进行初始化,然后通过 .add().addColor() 等方法为它添加控制选项,并在控制选项发生改变时在 .onChange() 中调用我们预先写好的回调函数来更新粒子样式。回调函数 ctrls 也很简单,就是通过 scene.getObjectByName("particles") 找到场景中已经创建好的粒子将它删除,然后使用新的参数再次调用 createStyledParticlesByPoints 来创建新的粒子。

const createStyledParticlesByPoints = (ctrls) => {
  const geom = new THREE.BufferGeometry();
  const material = new THREE.PointsMaterial({
    size: ctrls.size,
    transparent: ctrls.transparent,
    opacity: ctrls.opacity,
    color: new THREE.Color(ctrls.color),
    vertexColors: ctrls.vertexColors,
    sizeAttenuation: ctrls.sizeAttenuation
  });
  let veticsFloat32Array = []
  let veticsColors = []
  for (let x = -15; x < 15; x++) {
    for (let y = -10; y < 10; y++) {
        veticsFloat32Array.push(x * 4, y * 4, 0)
        const randomColor = new THREE.Color(Math.random() * ctrls.vertexColor)
        veticsColors.push(randomColor.r, randomColor.g, randomColor.b)
    }
  }
  const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3)
  const colors = new THREE.Float32BufferAttribute(veticsColors, 3)
  geom.attributes.position = vertices;
  geom.attributes.color = colors;
  const particles = new THREE.Points(geom, material);
  particles.name = 'particles';
  scene.add(particles)
}
// 创建属性控制器
const ctrls = new function () {
  this.size = 5;
  this.transparent = true;
  this.opacity = 0.6;
  this.vertexColors = true;
  this.color = 0xffffff;
  this.vertexColor = 0x00ff00;
  this.sizeAttenuation = true;
  this.rotate = true;
  this.redraw = function () {
    if (scene.getObjectByName("particles")) {
      scene.remove(scene.getObjectByName("particles"));
    }
    createStyledParticlesByPoints({
      size: ctrls.size,
      transparent: ctrls.transparent,
      opacity: ctrls.opacity,
      vertexColors: ctrls.vertexColors,
      sizeAttenuation: ctrls.sizeAttenuation,
      color: ctrls.color,
      vertexColor: ctrls.vertexColor
    });
  };
}
const gui = new dat.GUI();
gui.add(ctrls, 'size', 0, 10).onChange(ctrls.redraw);
gui.add(ctrls, 'transparent').onChange(ctrls.redraw);
gui.add(ctrls, 'opacity', 0, 1).onChange(ctrls.redraw);
gui.add(ctrls, 'vertexColors').onChange(ctrls.redraw);
gui.addColor(ctrls, 'color').onChange(ctrls.redraw);
gui.addColor(ctrls, 'vertexColor').onChange(ctrls.redraw);
gui.add(ctrls, 'sizeAttenuation').onChange(ctrls.redraw);

添加 dat.GUI 后,页面顶部就会出现一个对应参数可视化的控制器 🎮,用鼠标在上面下拉或滑动修改参数,就能实时更新到页面中了。赶快动手试试,看看修改不同参数对粒子的影响有何不同吧 🤩

method_2.png

知识点 💡 dat.GUI

dat.GUI 是一个轻量级的 JavaScript 图形用户界面控制库,它可以轻松地即时操作变量和触发函数,通过设定好的控制器去快捷的修改设定的变量。下面是它的一些基本使用方法:

// 初始化
const gui = new dat.GUI({ name: 'name'});
// 初始化控件属性
const ctrls = {
  name: 'acqui',
  speed: 0.5,
  color1: '#FF0000',
  color2: [0, 128, 255],
  color3: [0, 128, 255, 0.3],
  color4: { h: 350, s: 0.9, v: 0.3 },
  test: ',
  test2: ',
  cb: () => {},
  gender:true
};
// gui.add(控件对象变量名,对象属性名,其它参数),控制字符类型或数字
gui.add(ctrls, 'name');
// 缩放区间[0,100],变化步长10
gui.add(ctrls, 'speed', 0, 100, 10);
// 创建一个下拉选择
gui.add(ctrls, 'test', { 低速: 0.005, 中速: 0.01, 高速: 0.1 }).name('转速');
gui.add(ctrls, 'test2', ['低速', '中速', '高速']).name('转速2');
//  创建按钮
gui.add(ctrls, 'cb').name('按钮');
gui.add(ctrls, 'gender').name('性别');
// 控制颜色属性
gui.addColor(ctrls, 'color1');
// 通过name可设置别名
gui.addColor(ctrls, 'color2').name('颜色2');
// 创建一个Folder
const folder = gui.addFolder('颜色组');
folder.addColor(ctrls, 'color3');
folder.addColor(ctrls, 'color4');
//可以通过onChange方法来监听改变的值,从而修改样式
gui.addColor(ctrls, 'color2').onChange(callback);

📌 dat.GUI不仅能用到Three.js开发中,在其他需要实时修改参数查看效果的场景下都能用哦。 官网地址:github.com/dataarts/da…

③ 使用Canvas样式化粒子

THREE.js 提供了将 HTML5 画布 Canvas 转化为纹理的功能,利用这一特性,我们就可以创建个性化的 Canvas 来美化我们的粒子效果。在本文示例 createParticlesByCanvas 方法中,首先提供了一个 createCanvasTexture 的方法用来生成 Canvas 纹理,在该方法中,我们创建一个 Canvas 画布,然后在上面绘制一个彩色渐变圆环,最后使用 THREE.CanvasTexture 方法将其转化为可以在 Three.js 中渲染的纹理。然后使用该纹理,通过 map 属性将其传递给粒子材质 THREE.PointsMaterial。这样粒子就具有了如下图所示的 Canvas 纹理效果了。

注意 🔺 ,如果此时用鼠标旋转粒子时会发现粒子四周本该透明的地方是黑色的,而且前后粒子之间还存在穿透问题。此时需要给粒子材质设置 depthTest: truedepthWrite: false 设置这两个属性,以解决粒子显示正常的透明效果以及前后叠加层级问题

const createParticlesByCanvas = () => {
  // 使用canvas创建纹理
  const createCanvasTexture = () => {
    const canvas = document.createElement('canvas')
    const ctx = canvas.getContext('2d')
    canvas.width = 300
    canvas.height = 300
    ctx.lineWidth = 10;
    ctx.beginPath();
    ctx.moveTo(170, 120);
    var grd = ctx.createLinearGradient(0, 0, 170, 0);
    grd.addColorStop('0', 'black');
    grd.addColorStop('0.3', 'magenta');
    grd.addColorStop('0.5', 'blue');
    grd.addColorStop('0.6', 'green');
    grd.addColorStop('0.8', 'yellow');
    grd.addColorStop(1, 'red');
    ctx.strokeStyle = grd;
    ctx.arc(120, 120, 50, 0, Math.PI * 2);
    ctx.stroke();
    const texture = new THREE.CanvasTexture(canvas)
    texture.needsUpdate = true
    return texture
  }
  // 创建粒子系统
  const createParticles = (size, transparent, opacity, sizeAttenuation, color) => {
    const geom = new THREE.BufferGeometry()
    const material = new THREE.PointsMaterial({
      size: size,
      transparent: transparent,
      opacity: opacity,
      map: texture,
      sizeAttenuation: sizeAttenuation,
      color: color,
      depthTest: true,
      depthWrite: false
    })
    let veticsFloat32Array = []
    const range = 500
    for (let i = 0; i < 400; i++) {
      const particle = new THREE.Vector3(Math.random() * range - range / 2, Math.random() * range - range / 2, Math.random() * range - range / 2)
      veticsFloat32Array.push(particle.x, particle.y, particle.z)
    }
    const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3)
    geom.attributes.position = vertices
    const particles = new THREE.Points(geom, material)
    scene.add(particles)
  }
  createParticles(40, true, 1, true, 0xffffff)
}

method_3.gif

知识点 💡 Canvas纹理CanvasTexture

用于从 Canvas 元素中创建纹理贴图。

构造函数:

CanvasTexture(canvas: HTMLElement, mapping: Constant, wrapS: Constant, wrapT: Constant, magFilter: Constant, minFilter: Constant, format: Constant, type: Constant, anisotropy: Number )
  • canvas:将会被用于加载纹理贴图的 Canvas 元素。
  • mapping:纹理贴图将被如何应用到物体上。
  • wrapS:默认值是 THREE.ClampToEdgeWrapping
  • wrapT:默认值是 THREE.ClampToEdgeWrapping
  • magFilter:当一个纹素覆盖大于一个像素时贴图将如何采样,默认值为 THREE.LinearFilter
  • minFilter:当一个纹素覆盖小于一个像素时贴图将如何采样,默认值为 THREE.LinearMipmapLinearFilter
  • format:在纹理贴图中使用的格式。
  • type:默认值是 THREE.UnsignedByteType
  • anisotropy:沿着轴,通过具有最高纹素密度的像素的样本数。 默认情况下,这个值为 1。设置一个较高的值将会产生比基本的 mipmap 更清晰的效果,代价是需要使用更多纹理样本。

属性和方法

  • .isCanvasTexture[Boolean]:检查是否是 CanvasTexture 类型纹理的只读属性。
  • .needsUpdate[Boolean]:是否需要更新,默认值为 true,以便使得 Canvas中的数据能够载入。
  • 其他属性和方法继承于 Texture

④ 使用纹理贴图样式化粒子

自然,Three.js 中粒子材质也可以直接使用 THREE.TextureLoader 加载图片作为纹理进行粒子样式个性化设置。createParticlesByTexture 方法和 createParticlesByCanvas 除了在给 THREE.PointsMaterial 设置 map 属性之处有区别之外,其他地方完全相同,因此下面代码只保留了关键内容,详情可查看源码

const createParticlesByTexture = () => {
  const createParticles = (size, transparent, opacity, sizeAttenuation, color) => {
    // ...
    const material = new THREE.PointsMaterial({
      'size': size,
      'transparent': transparent,
      'opacity': opacity,
      // 加载自定义图片作为粒子纹理
      'map': new THREE.TextureLoader().load('/images/heart.png'),
      'sizeAttenuation': sizeAttenuation,
      'color': color,
      depthTest: true,
      depthWrite: false
    })
    // ...
  }
}

本文示例中采用了一张霓虹心形爱心 💖 图片作为粒子纹理,效果如下图所示:

method_4.gif

⑤ 从高级几何体创建粒子

THREE.Points基于几何体的顶点来渲染每个粒子的,利用这一特性我们就可以从高级几何体来创建几何体形状的粒子。下面示例中我们利用 THREE.SphereGeometry 来创建一个球形的粒子系统。为了营造出好看视觉效果效果,我们可以使用 Canvas 的渐变方法 createRadialGradient 创建出一种类似发光特效来作为粒子的纹理。

const createParticlesByGeometry = () => {
  // 创建发光canvas纹理
  const generateSprite = () => {
    const canvas = document.createElement('canvas');
    canvas.width = 16;
    canvas.height = 16;
    const context = canvas.getContext('2d');
    const gradient = context.createRadialGradient(canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2);
    gradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
    gradient.addColorStop(0.2, 'rgba(0, 255, 0, 1)');
    gradient.addColorStop(0.4, 'rgba(0, 120, 20, 1)');
    gradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
    context.fillStyle = gradient;
    context.fillRect(0, 0, canvas.width, canvas.height);
    const texture = new THREE.Texture(canvas);
    texture.needsUpdate = true;
    return texture;
  }
  // 创建立方体
  const sphereGeometry = new THREE.SphereGeometry(15, 32, 16);
  // 创建粒子材质
  const material = new THREE.PointsMaterial({
    color: 0xffffff,
    size: 3,
    transparent: true,
    blending: THREE.AdditiveBlending,
    map: generateSprite(),
    depthWrite: false
  })
  const particles = new THREE.Points(sphereGeometry, material)
  scene.add(particles)
}

method_5.png

⑥ 迷失太空

最后,我们利用上述汇总介绍的的粒子系统知识,并结合本专栏前几期的内容进行实践,打造一个趣味的 3D 页面,在无边无际的宇宙,宇航员跌向无尽深渊。

创建宇航员 👨‍🚀

首先使用 GLTFLoader 模型加载器加载宇航员 👨‍🚀 模型到场景中,并调整好模型在场景中的初始大小和位置。

const loader = new GLTFLoader();
loader.load('/models/astronaut.glb', mesh => {
  astronaut = mesh.scene;
  astronaut.material = new THREE.MeshLambertMaterial();
  astronaut.scale.set(.0005, .0005, .0005);
  astronaut.position.z = -10;
  scene.add(astronaut);
});

astronaut.png

创建宇宙粒子 🌌

使用上述创建粒子系统的方法,创建 1000 个粒子作为宇宙星系,并为它们设置限定范围内随机的位置,星系粒子纹理采用了一张径向渐变的贴图,将其添加到场景中。

const geom = new THREE.BufferGeometry();
const material = new THREE.PointsMaterial({
  color: 0xffffff,
  size: 10,
  alphaTest: .8,
  map: new THREE.TextureLoader().load('/images/particle.png')
});
let veticsFloat32Array = []
let veticsColors = []
for (let p = 0; p < 1000; p++) {
  veticsFloat32Array.push(
    rand(20, 30) * Math.cos(p),
    rand(20, 30) * Math.sin(p),
    rand(-1500, 0)
  );
  const randomColor = new THREE.Color(Math.random() * 0xffffff);
  veticsColors.push(randomColor.r, randomColor.g, randomColor.b);
}
const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
const colors = new THREE.Float32BufferAttribute(veticsColors, 3);
geom.attributes.position = vertices;
geom.attributes.color = colors;
const particleSystem = new THREE.Points(geom, material);
scene.add(particleSystem);

场景优化 ✨

根据场景内摄像机的位置以及宇宙星系粒子在Z轴方向的变化,添加合适参数的黑色雾化效果来营造出由近到远星系由亮到暗的变化效果,增强画面的真实性。接着,为了宇航员显示在场景中,根据实际参数增加一些光照效果。

// 雾化效果
scene.fog = new THREE.FogExp2(0x000000, 0.005);
// 设置光照
let light = new THREE.PointLight(0xFFFFFF, 0.5);
light.position.x = -50;
light.position.y = -50;
light.position.z = 75;
scene.add(light);
light = new THREE.PointLight(0xFFFFFF, 0.5);
light.position.x = 50;
light.position.y = 50;
light.position.z = 75;
scene.add(light);
light = new THREE.PointLight(0xFFFFFF, 0.3);
light.position.x = 25;
light.position.y = 50;
light.position.z = 200;
scene.add(light);
light = new THREE.AmbientLight(0xFFFFFF, 0.02);
scene.add(light);

知识点 💡 雾化效果Fog和FogExp2

为了增强场景的真实性,示例中使用了 FogExp2 雾化效果,那么 THREE.FogTHREE.FogExp2 有什么不同呢?

  • 雾Fog
    • 定义:表示线性雾,雾的密度是随着距离线性增大的,即场景中物体雾化效果随着随距离线性变化。
    • 构造函数:Fog(color, near, far)
      • .color:表示雾的颜色,场景中远处物体为黑色,场景中最近处距离物体是自身颜色,最远和最近之间的物体颜色是物体本身颜色和雾颜色的混合效果。
      • .near:表示应用雾化效果的最小距离,距离活动摄像机长度小于 .near 的物体将不会被雾所影响。
      • .far: 表示应用雾化效果的最大距离,距离活动摄像机长度大于 .far 的物体将不会被雾所影响。
  • 指数雾FogExp2
    • 定义:表示指数雾,即雾的密度随着距离指数而增大。
    • 构造函数:FogExp2(color, density)
      • .color:表示雾的颜色。
      • .density:表示雾的密度的增速,默认值为 0.00025

添加动画效果 🎦

整个场景的动画分为以下三个部分:

  • 粒子系统动画由以下两部分构成:
    • 粒子系统旋转动画:使用余弦函数 Math.cos(t) 改变粒子系统的 position 来创建旋转运动轨迹,其中 t 指定了旋转速度。
    • 粒子系统由近到远动画:通过遍历更改构成粒子的向量数组中表示每个粒子的 Z轴 方向的值来更新粒子系统实现。
  • 宇航员模型动画:
    • 宇航员旋转动画:通过修改宇航员在 xyz 三个坐标轴上的位置参数实现。
    • 宇航员由近到远动画:使用正弦函数 Math.sin(t) 来生成宇航员远近循环动画。
  • 渲染器和相机的页面重绘更新动画。
const updateParticles = () => {
  // 粒子系统旋转动画
  particleSystem.position.x = 0.2 * Math.cos(t);
  particleSystem.position.y = 0.2 * Math.cos(t);
  particleSystem.rotation.z += 0.015;
  camera.lookAt(particleSystem.position);
  // 粒子系统由近到远动画
  for (let i = 0; i < veticsFloat32Array.length; i++) {
    // 如果是Z轴值,则修改数值
    if ((i + 1) % 3 === 0) {
      const dist = veticsFloat32Array[i] - camera.position.z;
      if (dist >= 0) veticsFloat32Array[i] = rand(-1000, -500);
      veticsFloat32Array[i] += 2.5;
      const _vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
      geom.attributes.position = _vertices;
    }
  }
  particleSystem.geometry.verticesNeedUpdate = true;
}
const updateMeshes = () => {
  if (astronaut) {
    // 宇航员由近到远动画
    astronaut.position.z = 0.08 * Math.sin(t) + (camera.position.z - 0.2);
    // 宇航员旋转动画
    astronaut.rotation.x += 0.015;
    astronaut.rotation.y += 0.015;
    astronaut.rotation.z += 0.01;
  }
}
// 场景和相机更新
const updateRenderer = () => {
  const width = canvas.clientWidth;
  const height = canvas.clientHeight;
  const needResize = canvas.width !== width || canvas.height !== height;
  if (needResize) {
    renderer.setSize(width, height, false);
    camera.aspect = canvas.clientWidth / canvas.clientHeight;
    camera.updateProjectionMatrix();
  }
}
const tick = () => {
  updateParticles();
  updateMeshes();
  updateRenderer();
  renderer.render(scene, camera);
  requestAnimationFrame(tick);
  t += 0.01;
}

其中 rand 方法用于生成两数之间的随机数。

const rand = (min, max) => min + Math.random() * (max - min);

鼠标移动交互 🖱

为了增强页面的可交互性,可以添加一些鼠标效果。在 迷失太空 这个示例中,当鼠标 🖱 在页面上进行移动时,场景中的宇航员 👨‍🚀 以及相机 📷 的位置会根据鼠标的相反方向发生部分偏移。

window.addEventListener('mousemove', e => {
  const cx = window.innerWidth / 2;
  const cy = window.innerHeight / 2;
  const dx = -1 * ((cx - e.clientX) / cx);
  const dy = -1 * ((cy - e.clientY) / cy);
  camera.position.x = dx * 5;
  camera.position.y = dy * 5;
  astronaut.position.x = dx * 5;
  astronaut.position.y = dy * 5;
});

CSS样式优化 💥

最后,使用 CSS 在页面上添加一些装饰性文案和图片,其中的 GRAVITY 字样使用了一种免费的字体,大家可以选择自己喜欢的样式风格进行页面修饰,提升页面的整体视觉效果。

lost_in_space.png

到此,本文涉及 Three.js 粒子系统的全部内容就结束了,完整代码可访问以下地址查看。

源码地址:github.com/dragonir/th…

总结

本文中主要包含的知识点包括:

  • 使用 THREE.Sprite 创建粒子。
  • 使用 THREE.Points 创建粒子。
  • 创建样式化的粒子。
  • 使用 dat.GUI 动态控制页面参数。
  • 使用 Canvas 样式化粒子。
  • 使用纹理贴图样式化粒子。
  • 从高级几何体创建粒子。
  • 给场景添加 FogFogExp2 雾化效果。
  • 使用正弦余弦函数添加模型动画、鼠标移动动画等。

想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 👍

附录

参考