uni-app 中基于 three.js 的 3D 粒子动效实现

发布于 1周前 作者 bupafengyu 来自 Uni-App

uni-app 中基于 three.js 的 3D 粒子动效实现

一、背景

粒子特效是为模拟现实中的水、火、雾、气等效果由各种三维软件开发的制作模块,原理是将无数的单个粒子组合使其呈现出固定形态,借由控制器、脚本来控制其整体或单个的运动,模拟出现真实的效果。three.js是用JavaScript编写的WebGL的第三方库,three.js提供了丰富的API帮助我们去实现3D动效,本文主要介绍如何使用three.js实现粒子过渡效果,以及基本的鼠标交互操作。(注:本文使用的关于three.js的API都是基于版本r98的。)

粒子特效示例

二、实现步骤

1. 创建渲染场景scene

scene实际上相当于一个三维空间,用于承载和显示我们所定义的一切,包括相机、物体、灯光等。在实际开发时为了方便观察可添加一些辅助工具,比如网格、坐标轴等。

scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x05050c, 10, 60);
scene.add(new THREE.GridHelper(2000, 1)); // 添加网格

2. 添加照相机camera

THREE里面实现了几种相机:PerspectiveCamera(透视相机)、OrthographicCamera(正交投影相机)、CubeCamera(立方体相机或全景相机)和StereoCamera(3D相机)。本文介绍我们主要用到的PerspectiveCamera(透视相机):

  • 视觉效果是近大远小。
  • 配置参数 PerspectiveCamera(fov, aspect, near, far)。
    • fov:相机的可视角度。
    • aspect:相机可视范围的长宽比。
    • near:相对于深度剪切面的远的距离。
    • far:相对于深度剪切面的远的距离。
camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 5, 100);
camera.position.set(10, -10, -40);
scene.add(camera);

3. 添加场景渲染需要的灯光

three.js里面实现的光源:AmbientLight(环境光)、DirectionalLight(平行光)、HemisphereLight(半球光)、PointLight(点光源)、RectAreaLight(平面光源)、SpotLight(聚光灯)等。配置光源参数时需要注意颜色的叠加效果,如环境光的颜色会直接作用于物体的当前颜色。各种光源的配置参数有些区别,下面是本文案例中会用到的二种光源。

let ambientLight = new THREE.AmbientLight(0x000000, 0.4);
scene.add(ambientLight);

let pointLight = new THREE.PointLight(0xe42107);
pointLight.castShadow = true;
pointLight.position.set(-10, -5, -10);
pointLight.distance = 20;
scene.add(pointLight);

4. 创建、导出并加载模型文件loader

创建模型,可以使用three.js editor进行创建或者用three.js的基础模型生成类进行生成,相对复杂的或者比较特殊的模型需要使用建模工具进行创建(c4d、3dmax等)。

使用three.js editor进行创建,可添加基本几何体,调整几何体的各种参数(位置、颜色、材质等)。

three.js editor

使用模型类生成。

let geometryCube = new THREE.BoxBufferGeometry(1, 1, 1);
let materialCube = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
let cubeMesh = new THREE.Mesh(geometryCube, materialCube);
scene.add(cubeMesh);

导出需要的模型文件(此处使用的是obj格式的模型文件)。

加载并解析模型文件数据。

let onProgress = function (xhr) {
  if (xhr.lengthComputable) {
    // 可进行计算得知模型加载进度
  }
};
let onError = function () {};
particleSystem = new THREE.Group();
var texture = new THREE.TextureLoader().load('./point.png');
new THREE.OBJLoader().load('./model.obj', function (object) {
  // object 模型文件数据
}, onProgress, onError);

5. 将导入到模型文件转换成粒子系统Points

获取模型的坐标值。

拷贝粒子坐标值到新建属性position1上,这个作为粒子过渡效果的最终坐标位置。

给粒子系统添加随机三维坐标值position,目的是把每个粒子位置打乱,设定起始位置。

let color = new THREE.Color('#ffffff');
let material = new THREE.PointsMaterial({
  size: 0.2,
  map: texture,
  depthTest: false,
  transparent: true
});
particleSystem = new THREE.Group();
let allCount = 0;
for (let i = 0; i < object.children.length; i++) {
  let name = object.children[i].name;
  let _attributes = object.children[i].geometry.attributes;
  let count = _attributes.position.count;
  _attributes.positionEnd = _attributes.position.clone();
  _attributes.position1 = _attributes.position.clone();
  for (let i = 0; i < count * 3; i++) {
    _attributes.position1.array[i] = Math.random() * 100 - 50;
  }
  let particles = new THREE.Points(object.children[i].geometry, material);
  particleSystem.add(particles);
  allCount += count;
}
particleSystem.applyMatrix(new THREE.Matrix4().makeTranslation(-5, -5, -10));

6. 通过tween动画库实现粒子坐标从position到position1点转换

利用TWEEN的缓动算法计算出各个粒子每一次变化的坐标位置,从初始位置到结束位置时间设置为2s(可自定义),每次执行计算之后都需要将attributes的position属性设置为true,用来提醒场景需要更新,在下次渲染时,render会使用最新计算的值进行渲染。

let pos = { val: 1 };
tween = new TWEEN.Tween(pos).to({ val: 0 }, 2500).easing(TWEEN.Easing.Quadratic.InOut).onUpdate(callback);
tween.onComplete(function () {
  console.log('过渡完成complete');
});
tween.start();

function callback() {
  let val = this.val;
  let particles = particleSystem.children;
  for (let i = 0; i < particles.length; i++) {
    let _attributes = particles[i].geometry.attributes;
    let name = particles[i].name;
    if (name.indexOf('_') === -1) {
      let positionEnd = _attributes.positionEnd.array;
      let position1 = _attributes.position1.array;
      let count = _attributes.position.count;
      for (let j = 0; j < count * 3; j++) {
        _attributes.position.array[j] = position1[j] * val + positionEnd[j] * (1 - val);
      }
    }
    _attributes.position.needsUpdate = true; // 设置更新
  }
}

7. 添加渲染场景render

创建容器。

定义render渲染器,设置各个参数。

将渲染器添加到容器里。

自定义的渲染函数render,在渲染函数里面我们利用TWEEN.update去更新模型的状态。

调用自定义的循环动画执行函数animate,利用requestAnimationFrame方法进行逐帧渲染。

let container = document.createElement('div');
document.body.appendChild(container);
renderer = new THREE.WebGLRenderer({
  antialias: true,
  alpha: true
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setClearColor(scene.fog.color);
renderer.setClearAlpha(0.8);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement); // 添加webgl渲染器

function render() {
  particleSystem.rotation.y += 0.0001;
  TWEEN.update();
  particleSystem.rotation.y += (mouseX + camera.rotation.x) * .00001;
  camera.lookAt(new THREE.Vector3(-10, -5, -10));
  controls.update();
  renderer.render(scene, camera);
}

function animate() { // 开始循环执行渲染动画
  requestAnimationFrame(animate);
  render();
}

8. 添加鼠标操作事件实现角度控制

我们还可以添加鼠标操作事件实现角度控制,其中winX、winY分别为window的宽高的一半,当然具体的坐标位置可以根据自己的需求进行计算,具体的效果如下图所示。

鼠标操作效果

document.addEventListener('mousemove', onDocumentMouseMove, false);

function onDocumentMouseMove(event) {
  mouseX = (event.clientX - winX) / 2;
  mouseY = (event.clientY - winY) / 2;
}

三、优化方案

1. 减少粒子数量

随着粒子数量的增加,需要的计算每个粒子的位置和大小将会非常耗时,可能会造成动画卡顿或出现页面假死的情况,所以我们在建立模型时可尽量减少粒子的数量,能够有效提升性能。

在以上示例中,我们改变导出模型的精细程度,可以得到不同数量的粒子系统,当粒子数量达到几十万甚至几百万的时候,在动画加载时可以感受到明显的卡顿现象,这主要是由于fps比较低,具体的对比效果如下图所示,左边粒子数量为30万,右边粒子数量为6万,可以明显看出左边跳帧明显,右边基本保持比较流畅的状态。

粒子数量对比

2. 采用GPU渲染方式

编写片元着色器代码,利用webgl可以为canvas提供硬件3D加速,浏览器可以更流畅地渲染页面。目前大多数设备都已经支持该方式,需要注意的是在低端的设备上由于硬件设备原因,渲染的速度可能不及基于cpu计算的方式渲染。

四、总结

综上所述,实现粒子动效的关键在于计算、维护每个粒子的位置状态,而three.js提供了较为便利的方法,可以用于渲染整个粒子场景。当粒子数量极为庞大时,想要实现较为流畅的动画效果需要注意优化代码、减少计算等,也可以通过提升硬件配置来达到效果。本文中的案例为大家展示了3D粒子动效如何实现,大家可以根据自己的实际需求去制作更炫酷的动态效果。

开发环境 版本号 项目创建方式
three.js r98 使用three.js API

1 回复

在uni-app中集成three.js以实现3D粒子动效,可以通过以下步骤完成。以下是一个简单的代码示例,展示了如何在uni-app中使用three.js创建一个基本的3D粒子系统。

首先,确保你已经在项目中安装了three.js。你可以通过npm安装或者直接下载three.js库文件并引入。

# 使用npm安装three.js(如果你使用的是支持npm的uni-app环境)
npm install three

接下来,在你的uni-app项目的页面(例如pages/index/index.vue)中,编写以下代码:

<template>
  <view class="container">
    <canvas canvas-id="glCanvas" style="width: 100%; height: 100%;"></canvas>
  </view>
</template>

<script>
import * as THREE from 'three';

export default {
  data() {
    return {
      scene: null,
      camera: null,
      renderer: null,
      particles: null,
      particleSystem: null,
      animationId: null,
    };
  },
  mounted() {
    this.initThree();
    this.animate();
  },
  methods: {
    initThree() {
      // 创建场景
      this.scene = new THREE.Scene();

      // 创建相机
      this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
      this.camera.position.z = 5;

      // 创建渲染器
      this.renderer = new THREE.WebGLRenderer({ canvas: this.$refs.glCanvas });
      this.renderer.setSize(window.innerWidth, window.innerHeight);

      // 创建粒子材质
      const particleMaterial = new THREE.PointsMaterial({ color: 0xffffff });

      // 创建粒子几何体
      const particleGeometry = new THREE.BufferGeometry();
      const particleCount = 10000;
      const positions = new Float32Array(particleCount * 3);
      for (let i = 0; i < particleCount; i++) {
        positions[i * 3] = (Math.random() - 0.5) * 10;
        positions[i * 3 + 1] = (Math.random() - 0.5) * 10;
        positions[i * 3 + 2] = (Math.random() - 0.5) * 10;
      }
      particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

      // 创建粒子系统
      this.particleSystem = new THREE.Points(particleGeometry, particleMaterial);
      this.scene.add(this.particleSystem);
    },
    animate() {
      this.animationId = requestAnimationFrame(this.animate);

      // 更新粒子位置(这里可以添加你的动画逻辑)
      // 例如,简单地为每个粒子添加一个微小的随机位移
      const particles = this.particleSystem.geometry.attributes.position.array;
      for (let i = 0; i < particles.length; i += 3) {
        particles[i] += (Math.random() - 0.5) * 0.01;
        particles[i + 1] += (Math.random() - 0.5) * 0.01;
        particles[i + 2] += (Math.random() - 0.5) * 0.01;
      }
      this.particleSystem.geometry.attributes.position.needsUpdate = true;

      // 渲染场景
      this.renderer.render(this.scene, this.camera);
    },
  },
};
</script>

<style>
.container {
  width: 100%;
  height: 100%;
}
</style>

这段代码创建了一个基本的three.js场景,并在其中添加了一个粒子系统。粒子系统的位置在每个动画帧中随机更新,以创建简单的动效。请注意,这里的动画逻辑非常简单,你可以根据实际需求添加更复杂的动画效果。

回到顶部