V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
getui
V2EX  ›  前端开发

基于 three.js 的 3D 粒子动效实现

  •  
  •   getui · 2019-04-08 16:35:51 +08:00 · 2960 次点击
    这是一个创建于 2062 天前的主题,其中的信息可能已经有所发展或是发生改变。

    作者:个推 web 前端开发工程师 梁神

    一、背景

    粒子特效是为模拟现实中的水、火、雾、气等效果由各种三维软件开发的制作模块,原理是将无数的单个粒子组合使其呈现出固定形态,借由控制器、脚本来控制其整体或单个的运动,模拟出现真实的效果。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 进行创建,可添加基本几何体,调整几何体的各种参数(位置、颜色、材质等)。

    使用模型类生成。

    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 粒子动效如何实现,大家可以根据自己的实际需求去制作更炫酷的动态效果。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2805 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 05:48 · PVG 13:48 · LAX 21:48 · JFK 00:48
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.