0%

基于THREE.JS的粒子系统

​缘起于一次技术支持,效果戳这里 - 建议PC访问

主要技术问题

  • 将传统的描述3D模型的.obj文件转化成.json文件。

  • 多模型加载进度

  • 从.json文件中提取3D对象节点信息。

  • 根据节点数量构建粒子系统。

  • 3D模型位置调整。

  • 模型切换时的粒子运动算法

  • 后期图像处理

解决方案

  • 将传统的描述3D模型的.obj文件转化成.json文件。

    • 针对模型文件类型转化THREE官方提供了转换工具convert_obj_three,这是一个基于Python的运行程序,需要Python的运行环境,输入指令即可转化。
  • 多模型加载进度

    • JS加载文件完全为异步加载,同时加载多个文件时如何计算加载总进度是个问题。这里通过在多个文件loading的onProgress过程中同时向一个全局变量写入数据计算平均进度。代码如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      var _Progress = {};
      var onProgress = function (xhr, i) {
      if (xhr.lengthComputable) {
      var percentComplete = xhr.loaded / xhr.total * 100;
      _Progress[xhr.total] = percentComplete; //向_Progress写入进度以文件大小作为key
      percentComplete = 0;
      for (var key in _Progress) {
      percentComplete = percentComplete + _Progress[key];
      }
      document.getElementById("prossgressValue").innerText = (Math.round(percentComplete / _fileCount, 2) + '%');
      console.log(Math.round(percentComplete / _fileCount, 2) + '% downloaded');
      }
      };
  • 从.json文件中提取3D对象节点信息。

    • 第一步的文件类型转化也是在为这一步做准备,THREE读取的obj文件只有“面”信息,并没有点信息。转化成.json格式后可通过geometry的vertices对象获取节点数据。
  • 根据节点数量构建粒子系统。

    • 这里将初始化足够的坐标随机的节点用于映射模型的每个节点,并且构建一个二维数据来存储模型的坐标。第一个维度用于定位模型,第二个维度用于定位模型的某个节点。接下来将根据这个数据来渲染整个模型。
  • 3D模型位置调整。

    • 构建的3D模型并不能完全适配场景,需要针对场景做出放大、缩小、平移、旋转等操作。对于放大缩小和平移的操作只需要针对geometry的vertices对象的x,y,z属性做出加减乘除的操作就可以实现。针对旋转操作需要用到矩阵运算。这里THREE同样提供了创建旋转矩阵的API代码如下

      1
      object.geometry.applyMatrix(new THREE.Matrix4().makeRotationX(3.14 / 7.5))
  • 模型切换时的粒子运动算法

    • 针对模型切换时的粒子运动算法,这里我采用的是减速渐进法根据节点目前的位置与终点的位置差来计算速度。距离越远速度越快,当将要接近时速度趋近于0。该方法好处为可快速构建模型大致结构。缺点则为在构建出大致结构之后,用户主管上会认为动画接近或者已经结束。主观感受上粒子的运行动画时间较短,不够炫酷。若采用加速渐进法则正好相反,粒子在运动初期速度较慢,用户得知下个模型的细节所需的时间更长,更能吸引用户注意。但缺点则为会有较大的一段时间粒子运动较为凌乱。减速渐进法代码如下。

      1
      2
      3
      4
      5
      6
      for (var i = 0; i < Particles.length; i++) {
      var _dx = 0.1/*系数*/ * (Data[i][count].x /*目标位置*/ - Particles[i].position.x)/*当前位置*/;
      var _dy = 0.1 * (Data[i][count].y - Particles[i].position.y);
      var _dz = 0.1 * (Data[i][count].z - Particles[i].position.z);
      Particles[i].position.set(Particles[i].position.x + _dx, Particles[i].position.y + _dy, Particles[i].position.z + _dz)
      }/*改变坐标*/
  • 后期图像处理

    • 单纯的粒子特效远远不够炫酷,就需要一些后期的特效来处理图像,类似于一些,聚焦、模糊、光晕等特效,这里通过创建一个THREE.EffectComposer对象,并为该对象创建一些列的后期处理效果,用该对象的render方法来代替默认的render方法是图像得到充分的后期处理。不过消耗计算资源严重,谨慎选择后期特效。代码如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      function initEffectComposer() {

      composer = new THREE.EffectComposer(renderer);

      var renderPass = new THREE.RenderPass(scene, camera);
      // var bloomPass = new THREE.BloomPass(0.5);
      var effectFilm = new THREE.FilmPass(.5, .5, 1500, !1);
      var shaderPass = new THREE.ShaderPass(THREE.FocusShader)
      shaderPass.uniforms.screenWidth.value = window.innerWidth;
      shaderPass.uniforms.screenHeight.value = window.innerHeight;
      shaderPass.renderToScreen = true;

      composer.addPass(renderPass);
      composer.addPass(effectFilm);
      // composer.addPass(bloomPass);
      composer.addPass(shaderPass);
      }

总结

​整体来说实现起来并不是特别复杂,但同样在炫酷效果上需要更大量时间的去打磨。像这种炫酷的效果,通常来说更加考验设计功底,这也是某些行业,可视化设计已经比可视化工程师更金贵的原因。

优化

2018-2-27 更新

​我竟然发现了官方版的作品介绍

​这里边讲述了一些开发过程中遇到的问题,用到的主要技术,以及一些开发时间等等内容。这里针对和官方相比一些不足的地方做些优化。

粒子间过渡时候的缓动动画

​这部分本来是应用的自己的算法,也先后经历了几个版本的优化。

​算法本身其实存在一定的问题,没有完美的算法只有更好的算法。

​先上算法:

1
2
3
4
5
6
for (var i = 0; i < Particles.length; i++) {
var _dx = 0.1/*系数*/ * (Data[i][count].x /*目标位置*/ - Particles[i].position.x)/*当前位置*/;
var _dy = 0.1 * (Data[i][count].y - Particles[i].position.y);
var _dz = 0.1 * (Data[i][count].z - Particles[i].position.z);
Particles[i].position.set(Particles[i].position.x + _dx, Particles[i].position.y + _dy, Particles[i].position.z + _dz)
}/*改变坐标*/

​再说问题

​问题1: 如果大部分粒子已经十分接近目的地,仍然浪费计算力对粒子坐标进行微小的修改是十分不必要的。

​问题2:并没有像原站那样的一部分粒子随机延迟的效果。所有粒子同时移动,根据距离来确定速度,就导致距离远的粒子肯定较晚到达,就会造成这种效果,看起来十分不和谐。

ZHTTVis-1

​针对问题1,这里对粒子位置进行了抽样。如果大部分粒子已经十分接近目的地则停止坐标计算,节约了部分计算力。

​针对问题2,则采用了和原站一样的tween的解决方案。将粒子的坐标计算交给tween来完成。仅需要通知tween需要计算的坐标当前值,最终值,动画时长、等待时间等参数。

ZHTTVis-1

​然而这中解决方案导致页面非常卡。。。卡成PPT的那种。替换解决方案为通知tween动画当前的进度,(0-1)在Update会掉里对相应的粒子的坐标进行计算。性能得到了巨大的提升。(原因还未知,等确定了回来更)

ZHTTVis-1

性能优化

原站性能

ZHTTVis-1

优化之前

ZHTTVis-1

优化之后

ZHTTVis-1

​2018-3-21更新

​经排查造成性能差异的主要原因在于对象个数本站new了1W个point对象每个对象的位置修改都需要cpu通知gpu做计算,低速的通讯严重拖慢了站点的性能。这里通过将所有的Point对象合成一个对象,将原来的Point对象改为新的Point对象的一个vertice。这样就大大降低了CPU和GPU的通讯次数从而提升性能。

1
2
3
4
5
6
for (var i = 0; i < PARTICLES_SYSTEM_NUM; i++) {
PARTICLES_SYSTEM.vertices.push(new THREE.Vector3(Math.random() * CLOUDPARTICLES_AREA - CLOUDPARTICLES_AREA / 2, Math.random() * CLOUDPARTICLES_AREA - CLOUDPARTICLES_AREA / 2, Math.random() * CLOUDPARTICLES_AREA - CLOUDPARTICLES_AREA / 2));
}

let _PARTICLES_SYSTEM = new THREE.Points(PARTICLES_SYSTEM, particleMaterial);
scene.add(_PARTICLES_SYSTEM);

节点扩充

采用TessellateModifier对节点进行细分

2018-3-21-3.jpg

动画

2018-3-21.gif

核心公式

1
y=|x|sin(|x|)/10

核心代码

1
PARTICLES_SYSTEM.vertices[i].y=Data[i][count].y+DataDelta[i]*Math.sin(DataDelta[i]/CYCLE)+ value)*AMPLITUDE

DataDelta[i]可以映射为核心公式中的|x| ,CYCLE 为周期常量,AMPLITUDE为振幅常量,通过value在(0-2π)区间内的周期性变化来控制动画播放。### 移动端自适应

​由于移动端机能问题、为了流畅度只能牺牲一些节点数量,这里主要集中在最后一屏上。

代码

1
const PARTICLES_SYSTEM_NUM = mobile ? 11000 : 5000;