一、three.js

使用three.js 渲染canvas 实现粒子波浪律动效果

three.js 粒子波浪效果

实现过程:

1、构建粒子系统

       首先搭建出一个二维平面(x-z平面),每一个点位均匀铺在这个平面上

  const AMOUNTX = 50;        //X轴上的点位数量
  const AMOUNTY = 50;        //Y轴上的点位数量
  const SEPARATION = 100;   //每个点位间的距离
  const numParticles = AMOUNTX * AMOUNTY;
  const vertices = new Float32Array(numParticles * 3);   //存储每个粒子的三维坐标 每三个下标一组[x,y,z]
  const scales = new Float32Array(numParticles);         //存储每个粒子的缩放值
 //初始化粒子坐标
  let i = 0;
  for (let ix = 0; ix < AMOUNTX; ix++) {
    for (let iy = 0; iy < AMOUNTY; iy++) {
      vertices[i] = ix * SEPARATION - (AMOUNTX * SEPARATION) / 2;     // x
      vertices[i + 1] = 0;                          // y,初始值为 0,后续动态更新
      vertices[i + 2] = iy * SEPARATION - (AMOUNTY * SEPARATION) / 2; // z
      i += 3;
    }
  }

2、创建粒子材质纹理

import * as THREE from 'three';
import g from '@/assets/gradient.png';    //导入纹理材质

const TextureLoader = new THREE.TextureLoader();    //创建纹理加载器
const material = new THREE.PointsMaterial({         //创建点材质
    size: 5,                       //设置大小
    sizeAttenuation: false,         //粒子大小是否随相机位置衰减
    transparent: true,             //开启透明
    opacity: 1,                    //透明程度
    blending: THREE.AdditiveBlending,    
    depthWrite: false,
    map: TextureLoader.load(g)
});

3、基于上面两步得到的粒子几何位置与材质纹理构建一个几何体对象 

  //three.js提供的缓冲类 便于处理大规模的几何体
  const geometry = new THREE.BufferGeometry();

  //给几何体设置点位坐标与每个点位的缩放值
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
  geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));
  
  //应用点模型
  const points = new THREE.Points(geometry, material);

4、创建场景

         three.js创建的场景对象Scene是一个容器 ,用于组织管理三维空间中的所有对象

        通过将对象添加到场景中,Three.js 的渲染器可以将这些对象渲染为二维图像,从而实现三维场景的可视化

const scene = new THREE.Scene();                  //创建场景容器
scene.background = new THREE.Color(0x000000);     //设置场景背景
scene.fog = new THREE.FogExp2(328972, 5e-4)       //添加烟雾

5、创建相机

three.js提供了两种类型的相机:
①OrthographicCamera               正投影相机

②PerspectiveCamera                 透视投影相机   (物体大小随距离发生变化)

PerspectiveCamera 在3D场景渲染中更为常见

相机的可见空间可以视为一个视锥体:

参考文章资料:

掌握视锥体原理:3D图形中的视觉空间与渲染规则-CSDN博客

three.js examples

const camera = new THREE.PerspectiveCamera({
    80,                                        //设定相机的fov
    window.innerWidth / window.innerHeight,    //视口宽高比
    1,                                         //近裁剪面
    5e4                                        //远裁剪面
})

camera.position.set(0, 0, 1e3);                //相对于场景中心的位置
camera.lookAt(new THREE.Vector3(0,0,0));       //设定相机朝向

6、创建渲染器等参数设置

   new THREE.WebGLRenderer() 得到的渲染器可以将3D模型渲染至 <canvas>容器中

   详细过程可以参阅three.js相关源码与资料

// 创建渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.autoClear = false
renderer.setClearColor(scene.fog.color)

renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
this.$refs.threeContainer.appendChild(renderer.domElement);

// 添加粒子系统
const particles = GetFlatGeometry();
scene.add(particles);

// 添加环境光
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);

// 添加点光源
const pointLight = new THREE.PointLight(0xffffff, 1, 1000);
pointLight.position.set(500, 500, 500);
scene.add(pointLight);

7、添加动画效果

采用递归方法,结合requestAnimationFrame方法,在浏览器绘制下一帧之前更新该几何对象的位置等属性

requestAnimationFrame在EventLoop的什么阶段执行? - 王铁柱6 - 博客园

const animate = () => {
  requestAnimationFrame(animate); 

  // 更新波浪效果
  const positions = particles.geometry.attributes.position.array;
  for (let i = 0; i < positions.length; i += 3) {
    const x = positions[i];
    const z = positions[i + 2];
    positions[i + 1] = Math.sin((x + time) * 0.5) * 50 + Math.cos((z + time) * 0.5) * 50 -350;
  }
  particles.geometry.attributes.position.needsUpdate = true;

  // 渲染场景
  renderer.render(scene, camera);
  time += 0.03; // 控制波浪的动态变化速度
};

8、让渲染出的canvas适应浏览器视窗的变化

// 响应窗口大小调整
window.addEventListener("resize", () => {
  renderer.setSize(window.innerWidth, window.innerHeight);
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
});

我将其封装成了一个vue组件:ParticleSystem.vue

完整代码如下:

//GetFlatGeometry.js
import * as THREE from 'three';
import g from '@/assets/gradient.png';

export default function GetFlatGeometry() {
  const AMOUNTX = 70;
  const AMOUNTY = 70;
  const SEPARATION = 100;
  const numParticles = AMOUNTX * AMOUNTY;
  const vertices = new Float32Array(numParticles * 3);   //存储每个粒子的三维坐标 每三个下标一组[x,y,z]
  const scales = new Float32Array(numParticles);         //存储每个粒子的缩放值



  const TextureLoader = new THREE.TextureLoader();
  const material = new THREE.PointsMaterial({
    size: 5,
    sizeAttenuation: false,
    transparent: true,
    opacity: 1,
    blending: THREE.AdditiveBlending,
    depthWrite: false,
    map: TextureLoader.load(g)
  });


  let i = 0;
  for (let ix = 0; ix < AMOUNTX; ix++) {
    for (let iy = 0; iy < AMOUNTY; iy++) {
      vertices[i] = ix * SEPARATION - (AMOUNTX * SEPARATION) / 2; // x
      vertices[i + 1] = 0; // y,初始值为 0,后续动态更新
      vertices[i + 2] = iy * SEPARATION - (AMOUNTY * SEPARATION) / 2; // z
      i += 3;
    }
  }

  const geometry = new THREE.BufferGeometry();
  geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
  geometry.setAttribute('scale', new THREE.BufferAttribute(scales, 1));

  const points = new THREE.Points(geometry, material);
  return points;
}

//ParticleSystem.vue
<template>
    <div ref="threeContainer" class="three-container"></div>
  </template>
  
  <script>
  import * as THREE from "three";
  import GetFlatGeometry from "@/utils/GetFlatGeometry.js";

  export default {
    mounted() {
      this.initThree();
    },
    methods: {
      initThree() {
        // 创建场景
        const scene = new THREE.Scene();
        scene.background = new THREE.Color(0x000000);
        scene.fog = new THREE.FogExp2(328972, 5e-4)

        // 创建相机
        const camera = new THREE.PerspectiveCamera(
          80,
          window.innerWidth / window.innerHeight,
          1,
          5e4
        );
        camera.position.set(0, 0, 1e3);
        camera.lookAt(new THREE.Vector3(0,0,0)); // 固定相机朝向
        
        // const axesHelper = new THREE.AxesHelper(500)
        // scene.add(axesHelper)
        // 创建渲染器
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.autoClear = false
        renderer.setClearColor(scene.fog.color)
        
        renderer.shadowMap.enabled = true
        renderer.shadowMap.type = THREE.PCFSoftShadowMap
        this.$refs.threeContainer.appendChild(renderer.domElement);
  
        // 添加粒子系统
        const particles = GetFlatGeometry();
        scene.add(particles);
        
        // 添加环境光
        const ambientLight = new THREE.AmbientLight(0x404040);
        scene.add(ambientLight);
  
        // 添加点光源
        const pointLight = new THREE.PointLight(0xffffff, 1, 1000);
        pointLight.position.set(500, 500, 500);
        scene.add(pointLight);
  
        // 动画循环
        let time = 0;
        const animate = () => {
          requestAnimationFrame(animate);
  
          // 更新波浪效果
          const positions = particles.geometry.attributes.position.array;
          for (let i = 0; i < positions.length; i += 3) {
            const x = positions[i];
            const z = positions[i + 2];
            positions[i + 1] = Math.sin((x + time) * 0.5) * 50 + Math.cos((z + time) * 0.5) * 50 -350;
          }
          particles.geometry.attributes.position.needsUpdate = true;
  
          // 渲染场景
          renderer.render(scene, camera);
          time += 0.03; // 控制波浪的动态变化速度
        };
        animate();
  
        // 响应窗口大小调整
        window.addEventListener("resize", () => {
          renderer.setSize(window.innerWidth, window.innerHeight);
          camera.aspect = window.innerWidth / window.innerHeight;
          camera.updateProjectionMatrix();
        });
      },
    },
  };
  </script>
  
  <style scoped>
  .three-container {
    width: 100%;
    height: 100vh;
  }

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

 之后便可直接在其他页面上直接导入使用,另外需要注意导入后的页面元素层级关系,避免某些元素的原有布局被影响

    Logo

    技术共进,成长同行——讯飞AI开发者社区

    更多推荐