particle背后的故事!

第一次尝试使用GLSL来对页面效果进行处理,所以把实现的过程记录下来,方便自己下次查阅。如果你在这篇文章中发现错误,请帮助我纠正,谢谢。

场景构建

在页面中放置将近10w个particle,最开始的考虑将particle分为两个部分,一部分承载用户信息,一部分做装饰。对于在移动端渲染10w个particle性能方面是值得重视的地方,我采取的是承载用户信息的只有3000个左右,使用cpu操作。剩下的装饰particle都是用gpu做动画处理,这样页面会流畅很多。(最好的我觉的都用gpu去绘制,然后通过一种方式去判断这个particle是否可以点击,然后将对应承载信息放入点击的particle)

场景视觉效果&动画实现

  • particle 绘制

    particle的生成,我这里直接是用threejs提供的THREE.Points()方法去创建。

    代码如下:

    let geometry = createGeometry();
    
    let shaderMaterial = createMaterial();
    
    let particles = new THREE.Points(geometry, shaderMaterial);
      
    

    其中在创建Geometry的时候,我是创建了一个THREE.BufferGeometry(),因为我需要对particles的位置、颜色、大小进行动画操作,创建一个BufferGeometry会方便我们最后续的操作。

    另外创建Material时,没有使用threejs提供的默认Material,而是使用THREE.ShaderMaterial(),这样我们就可以自己用GLSL写shader,这样对particles的控制会更加灵活。

  • particle 颜色

    定义两个基础颜色,然后对两个进行mix,来生成不同的颜色。(其中我将一个颜色的g值,再进行随机)

    代码如下:

    attribute vec3 a_randoms;
      
    v_color =  mix(vec3(194.0, 236.0 * random, 250.0), vec3(249.0, 178.0, 116.0), a_randoms.x) / 255.0;
    
  • noise

    在讲particle动画的前,先介绍一个非常牛b的东西noise(噪点)。

    • 什么是noise?

      其实简单理解就是相当于javascript中Math.random()。但是在GLSL中没有Math.random()方法,所以我们要自己去实现一个随机方法。

      在网上有一个神奇的公式,大多数GLSL程序都会使用它。公式如下:

       float rand(vec2 co){
          return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
      }
      

      简单来理解下这个公式是干嘛的:‘传入一个坐标值co.xy,然后与向量vec2(12.9898,78.233)点乘,避免某个位置分布密集,然后在利用sin or cos打乱点的位置,使其分布更加均匀,接着与一个超级大的数相乘后取小数部分,这样就得到一个(0.0-1.0)的数’。 然后就实现了随机数效果。

      渲染效果图:

      图1
      图1

      但是这个简单的随机效果并不是我们想要的,我们需要的是更加平滑的效果。我们可以先来了解下强大的Perlin Noise。具体描述可以点击链接来查看,我们来看渲染出来的效果。

      图2
      图2

      但在这个项目中我使用的是Simplex Noise,Simplex Noise是Ken Perlin创造Perlin Noise后,对算法进行优化的到结果。

      这里有一个GLSL的版本https://github.com/ashima/webgl-noise

    • noise能干什么?

      我们可以用nosie轻松的做出灼烧,溶解,云雾,模糊,扭曲,随机地形等效果。如下图:

      图3
      图3

      图4
      图4

      图3引用: https://www.shadertoy.com/view/lsf3RH

      图4引用:https://www.shadertoy.com/view/MdXyzX

      在项目中我就用noise做了一个表层的起伏效果。静态效果如下:

      图5
      图5

  • particle 组成的形状

    刚开始我把所有的particle组成了一个正方体:

    但有几点不好:

    1、carmea拉到远处的时候,画面很生硬。

    2、页面进入的时候有个爆炸效果,当particle炸开的时候,开始有一瞬间会看到一个正方体出现。

    基于这两点就换了一种,将particle组成一个球体,在实现球体的过程中出现了一些问题,当时不知道怎么将particle拼成一个球体,最后在网上找了一个计算方法,然后改了一下。先给定particle的初始位置,代码如下:

    // 初始位置 球体排列
    let phi = rand() * 2 * Math.PI;
    let cosTheta = rand() * 2 - 1;
    
    let u = rand();
    
    let theta = Math.acos(cosTheta);
    let r = Math.pow(u, 0.4) * particleFieldRadius;
    
    defaultPos.push({
      x: r * Math.sin(theta) * Math.cos(phi),
      y: r * Math.sin(theta) * Math.sin(phi),
      z: r * Math.cos(theta)
    });
      
    

    正方体与球体对比:

    图6
    图6

  • particle 动画

    particle的绘制与排列都做完了,然后对particle进行动画&效果的操作。

    • particle 发光效果

      为了让particle能看的更有感觉点,然后我用GLSL对particle进行效果的处理。测试阶段的时候绘制出了很多效果,但是总有不满意的地方,效果如下:

      图7
      图7

      最后采用了用一张贴图去做发光的效果,贴图如下:

      图8
      图8

      实现代码如下:

      uniform sampler2D u_alphaMap;
      float alpha = pow(texture2D(u_alphaMap, vec2(distanceToCenter, 0.5)).r * v_alpha * 1.5, 2.0 - 	v_alpha);
          
      vec4 color = vec4(v_color, alpha);	 
      
    • particle 噪点效果

      因为particle上承载着图片,所以在图片上加了一个简单的噪点效果,让图片看起来不会很死板。实现如下:

          vec4 color = vec4(v_color, alpha);
          color.r *= .9 + randNoise * .2;
          color.g *= .9 + randNoise * .2;
          color.b *= .9 + randNoise * .2;
      	    
          gl_FragColor = color;
      

      tips 其中randNoise的值就是上述noise中的简单随机。rand()

    • particle 爆炸效果

      爆炸效果实现的比较简单,大致思路就是,因为是个球体,所以设置一个爆开的半径,然后实时去改变particle的position。

      伪代码如下:(其中easeOutBack是为了让爆炸看起来更流畅)

       if (sharedProperties.prevExplosionRatio < sharedProperties.explosionRatio) {
              let particlesAttr = particles.geometry.attributes;
              let particleRatio;
              for (let i = 0, i3 = 0; i < particleCount; i++, i3 += 3) {
              	let rand = (i * 12.56162) % 1;
                  particleRatio = ease.easeOutBack(math.clampRange(rand * 0.1, 1 - rand * 0.1, sharedProperties.explosionRatio));
                  particlesAttr.position.array[i3] = defaultPos[i].x * particleRatio;
                  particlesAttr.position.array[i3 + 1] = defaultPos[i].y * particleRatio;
                  particlesAttr.position.array[i3 + 2] = defaultPos[i].z * particleRatio;
              }
              particles.geometry.attributes.position.needsUpdate = true;
          }
      

      tips: 其实应该还有更好的动画实现方案吧?

    • 外层 particle 波浪起伏效果

      在做动态效果的时候,觉得球体外侧太平了,所以想增加一些效果,让它看起来更有感觉点。说起webgl中的动态效果,最好的就是用niose做了,上面已经讲过noise了。最后想着做一个波浪起伏的效果。大致实现思路:通过noise来控制粒子的起伏效果。设置振幅的高低,然后改变time,使用noise改变particle的位置。实现效果如图5所示。

      实现如下:

       float lowFeqNoise = snoise4(vec4(position * 2.0, u_noiseTimes.x));
       float highFeqNoise = snoise4(vec4(position * 10.0 + 10.0, u_noiseTimes.y));
           
       offset += lowFeqNoise;
      
       offset += highFeqNoise;
      
       vec3 pos = position;
      
       pos *= offset;
      
       vec4 mvPosition = modelViewMatrix * vec4( pos, 1.0 );
           
       gl_Position = projectionMatrix * mvPosition;
      
  • camera 平滑运动

    然后考虑camera的运动,为了让相机能平滑的过度,这里我直接使用了TweenLite,具体使用文档可参考:https://greensock.com/docs/TweenLite

  • postprocessing

    为了让页面的边界地方看起来平滑点,所以对整个页面进行了处理,让它看起来有点虚的效果。如图所示:

    图9
    图9

    改变rgb是为了让元素周围会有色彩的围绕的感觉。

    实现大致如下:

     uniforms: {
      'u_texture': new THREE.WebGLRenderTarget(1, 1),
      ....
     }
    
     vec2 rgbShiftDelta = pow(0.05 + distToCenter, 0.1) * toCenter * 0.0025;
     vec4 rgbShiftRed = texture2D(u_texture, v_uv - rgbShiftDelta);
     vec4 rgbShiftGreen = texture2D(u_texture, v_uv);
     vec4 rgbShiftBlue = texture2D(u_texture, v_uv + rgbShiftDelta);
       
     gl_FragColor = vec4(...);
    

场景功能实现

  • particle的点击选中

    在3d空间中要做事件的操作,必定少不了射线检测,threejs提供了检测的方法THREE.Raycaster()

    let raycaster = new THREE.Raycaster()
      touch.x = (e.clientX / window.innerWidth)  * 2 - 1;
      touch.y = -(e.clientY / window.innerHeight)  * 2 + 1;
    	
      let vector = new THREE.Vector3(touch.x, touch.y, 0.5).unproject(camera);
    	
      raycaster.set(camera.position, vector.sub(camera.position).normalize());
      raycaster.params.Points.threshold = 200;
      raycaster.setFromCamera(touch, camera);
    	
      let intersects = raycaster.intersectObject(particle);
    	
      if (intersects.length > 0) {
       //..
      }
    
    

    tips: 我之前在做点击操作的时候,有时候会点不中particle,这里调整一下Points.threshold的值就好了。

  • camera轨迹计算

    当用户从一个particle切换到另一个particle的时候,camera有个平滑的过度的效果。当时我想了一种方案,就是将两个particle的信息位置直接交换,然后设定一段固定的camera动画。想象是很美好的,但是实现出来后,发现动画有点生硬,而且移动效果太low,有时会出现,两个particle挨的比较近,当点击的时候,camera会移动很久。所以就放弃了这个low low的办法。

    后来想了一种计算两个particle到相机的距离,然后在做camera的运动,通过raycaster我们知道了,点击particle的位置和index,也知道当前显示particle的位置和index,剩下的就是数学问题了。实现如下:

    function goToPoint (distance, pointIndex, time, cb) {
        let fromLookAt = camera.target.clone();
        let toLookAt = (new THREE.Vector3()).fromArray(particles.geometry.attributes.position.array, pointIndex * 3);
        let length = distance;  // 最终距离相机的距离
        let dir = fromLookAt.sub(toLookAt).normalize().multiplyScalar(length);
        let c = toLookAt.clone().add(dir);
        TweenLite.to(camera.position, time, {
            x: c.x,
            y: c.y,
            z: c.z,
            ease: Power3.easeOut,
            onComplete: function () {
              cb && cb();
            }
        });
    }
    

未实现的优化方案

  • 内层 particle gpu 绘制

    在这个项目中,我承载信息的particle的动画都是直接在cpu中完成的,假设有要求承载的信息的particle数量为5000或者更多,我觉得我现在的方案可能在手机上会炸吧。我将尝试改进,将承载信息的particle的动画用glsl完成。

  • particle的检测方式

    在这个项目中我是直接对3000个点进行射线检测,性能其实不是太好,我想了一个简单的思路具体还未实现,就是对屏幕可视区域的粒子做动态检测,将那些离camera远的粒子忽略,也就是区域划分吧。这样我想可能比现在的检测的方案要好一点。

  • 文案3d particle 切换

    在这个项目中文案的切换是直接利用的图片,然后动画的形式比较少,只有简单的唯一和淡入淡出,可以尝试将文案粒子化,然后用noise去做一个高级的动画切换。文字的3d切换可以看一下我另一篇文章:3d文字动态效果

所使用框架/工具

原文地址:https://flowers1225.com/lessons/2018/06/12/1

WEBGL学习网(WebGLStudy.COM)专注提供WebGL 、ThreeJS、BabylonJS等WEB3D开发案例源码下载。
声明信息:
1. 本站部分资源来源于用户上传和网络,如有侵权请邮件联系站长:1218436398@qq.com!我们将尽快处理。
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源打赏售价用于赞助本站提供的服务支出(包括不限服务器、网络带宽等费用支出)!
7.欢迎加QQ群学习交流:549297468 ,或者搜索微信公众号:WebGL学习网
WEBGL学习网 » particle背后的故事!

发表评论

提供优质的WebGL、ThreeJS源码

立即查看 了解详情