用 Three.js 打造炫酷波浪粒子背景动画:从原理到实现

用 Three.js 打造炫酷波浪粒子背景动画:从原理到实现

在现代 Web 开发中,动态背景是提升页面视觉冲击力的关键元素。波浪粒子动画以其流畅的运动轨迹、立体的空间感和可定制性,成为很多高端网站的首选。本文将带大家从零开始,用 Three.js 实现一个可配置的 3D 波浪粒子背景,并深入解析其核心原理。

一、效果预览

先看看最终实现的效果:页面中布满均匀分布的粒子,它们会沿着 X、Y 轴形成周期性的波浪运动,粒子大小随波浪同步变化,整体呈现出流动的 3D 视觉效果。支持自定义粒子数量、颜色、波浪强度等参数,适配不同场景需求。

二、技术选型:为什么选择 Three.js?

Three.js 是 Web 端最成熟的 3D 图形库,它封装了 WebGL 的底层 API,让我们无需深入理解复杂的图形学原理,就能快速实现 3D 效果。相比传统的 Canvas 动画,Three.js 具备以下优势:

  • 原生支持 3D 空间坐标系,轻松实现立体效果
  • 内置粒子系统、着色器等组件,适合批量粒子动画
  • 性能优化出色,支持大量粒子同时运动不卡顿
  • 丰富的材质和渲染选项,视觉表现力更强

三、核心原理拆解

1. 粒子系统基础

粒子动画的核心是批量粒子的位置和大小动态更新 。我们通过创建一个粒子集合(THREE.Points),并实时修改每个粒子的位置坐标和缩放比例,再通过渲染器持续绘制,形成动画效果。

2. 波浪运动数学模型

波浪效果的本质是正弦函数的周期性变化 。正弦函数Math.sin(x)的值在 [-1,1] 之间周期性波动,正好可以模拟波浪的起伏:

  • 粒子 Y 轴位置 = 正弦函数值 × 波浪幅度
  • 粒子大小 = 正弦函数值 × 缩放系数 + 基准值(确保粒子不会消失)

通过给 X 轴和 Y 轴的正弦函数设置不同的频率(0.3 和 0.5),可以形成更复杂的波浪叠加效果。

3. Three.js 核心组件分工

  • 场景(Scene) :3D 世界的容器,所有可见元素都需要添加到场景中
  • 相机(PerspectiveCamera) :模拟人眼的视角,决定我们能看到什么
  • 渲染器(WebGLRenderer) :将 3D 场景渲染到页面的 DOM 元素上
  • 几何体(BufferGeometry) :存储粒子的位置、大小等数据,高效传输给 GPU
  • 着色器材质(ShaderMaterial) :自定义粒子的渲染规则(大小、颜色、形状)

四、完整实现步骤

1. 项目初始化与依赖安装

首先确保项目中安装了 Three.js:

bash 复制代码
npm install three --save

2. 组件结构设计

我们将实现一个可复用的 Vue 组件,通过 props 暴露配置项,方便在不同场景中使用:

  • 粒子数量(X 轴 / Y 轴)
  • 粒子颜色
  • 波浪幅度(通过代码内置,也可扩展为 props)

3. 核心代码实现

js 复制代码
<template>
  <!-- 3d粒子背景 -->
  <div id="threeBg"></div>
</template>

<script>
import * as THREE from "three";
export default {
  props: {
    //控制x轴波浪的长度
    amountX: {
      type: Number,
      default: 50,
    },
    //控制y轴波浪的长度
    amountY: {
      type: Number,
      default: 50,
    },
    //控制点颜色
    color: {
      type: String,
      default: "#264046",
    },
  },
  data() {
    return {
      SEPARATION: 100,
      container: "",
      camera: "",
      scene: "",
      renderer: "",
      particles: "",
      count: 0,
      mouseX: "",
      windowHalfX: "",
    };
  },
  mounted() {
    this.windowHalfX = window.innerWidth / 2;
    this.init();
    this.animate();
  },
  methods: {
    init() {
      this.container = document.createElement("div");
      document.getElementById("threeBg").appendChild(this.container);

      //创建透视相机
      this.camera = new THREE.PerspectiveCamera(
        100, //摄像机视锥体垂直视野角度
        window.innerWidth / window.innerHeight, //摄像机视锥体长宽比
        1, //摄像机视锥体近端面
        10000 //摄像机视锥体远端面
      );

      //设置相机z轴视野
      this.camera.position.z = 1000;

      //创建场景
      this.scene = new THREE.Scene();
      const numParticles = this.amountX * this.amountY;
      const positions = new Float32Array(numParticles * 3);
      const scales = new Float32Array(numParticles);

      let i = 0,
        j = 0;

      // 初始化粒子位置和大小
      for (let ix = 0; ix < this.amountX; ix++) {
        for (let iy = 0; iy < this.amountY; iy++) {
          positions[i] =
            ix * this.SEPARATION - (this.amountX * this.SEPARATION) / 2; // x
          positions[i + 1] = 0; // y
          positions[i + 2] =
            iy * this.SEPARATION - (this.amountY * this.SEPARATION) / 2; // z
          scales[j] = 1;
          i += 3;
          j++;
        }
      }

      //是面片、线或点几何体的有效表述。包括顶点位置,面片索引、法相量、颜色值、UV 坐标和自定义缓存属性值。使用 BufferGeometry 可以有效减少向 GPU 传输上述数据所需的开销
      const geometry = new THREE.BufferGeometry();
      geometry.setAttribute(
        "position",
        new THREE.BufferAttribute(positions, 3)
      );
      geometry.setAttribute("scale", new THREE.BufferAttribute(scales, 1));

      //着色器材质(ShaderMaterial),设置球的大小,颜色,等
      const material = new THREE.ShaderMaterial({
        uniforms: {
          //设置球的颜色
          color: { value: new THREE.Color(this.color) },
        },
        //控制球的大小
        vertexShader:
          "attribute float scale; void main() {vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );gl_PointSize = scale * ( 300.0 / - mvPosition.z );gl_Position = projectionMatrix * mvPosition;}",
        fragmentShader:
          "uniform vec3 color;void main() {if ( length( gl_PointCoord - vec2( 0.5, 0.5 ) ) > 0.475 ) discard;gl_FragColor = vec4( color, 1.0 );}",
      });

      //一个用于显示点的类。
      this.particles = new THREE.Points(geometry, material);
      //往场景中添加点
      this.scene.add(this.particles);
      //alpha - canvas是否包含alpha (透明度)。默认为 false。
      //渲染器的背景色默认为黑色,设置渲染器的背景色为透明
      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
      this.renderer.setPixelRatio(window.devicePixelRatio);
      this.renderer.setClearAlpha(0);
      this.renderer.setSize(window.innerWidth, window.innerHeight);
      this.container.appendChild(this.renderer.domElement);
      this.container.style.touchAction = "none";
      window.addEventListener("resize", this.onWindowResize);
    },
    render() {
      this.camera.position.x = 100;
      this.camera.position.y = 400;
      this.camera.lookAt(this.scene.position);
      const positions = this.particles.geometry.attributes.position.array;
      const scales = this.particles.geometry.attributes.scale.array;

      // 设置粒子位置和大小
      let i = 0,
        j = 0;
      for (let ix = 0; ix < this.amountX; ix++) {
        for (let iy = 0; iy < this.amountY; iy++) {
          positions[i + 1] =
            Math.sin((ix + this.count) * 0.3) * 50 +
            Math.sin((iy + this.count) * 0.5) * 50;
          scales[j] =
            (Math.sin((ix + this.count) * 0.3) + 1) * 10 +
            (Math.sin((iy + this.count) * 0.5) + 1) * 10;
          i += 3;
          j++;
        }
      }
      this.particles.geometry.attributes.position.needsUpdate = true;
      this.particles.geometry.attributes.scale.needsUpdate = true;
      this.renderer.render(this.scene, this.camera);
      this.count += 0.1;
    },
    onWindowResize() {
      this.windowHalfX = window.innerWidth / 2;
      this.camera.aspect = window.innerWidth / window.innerHeight;
      this.camera.updateProjectionMatrix();
      this.renderer.setSize(window.innerWidth, window.innerHeight);
    },
    animate() {
      requestAnimationFrame(this.animate);
      this.render();
    },
  },
};
</script>

<style lang="scss" scoped>
#threeBg {
  width: 100%;
  height: 100%;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>
相关推荐
琉-璃6 小时前
vue3+ts 任意组件间的通信 mitt的使用
前端·javascript·vue.js
胖虎2657 小时前
Vue3 多入口项目实战:如何优雅管理多个独立业务模块
vue.js
小左OvO9 小时前
基于百度地图JSAPI Three的城市公交客流可视化(二)——区域客流
前端·javascript·vue.js
小左OvO9 小时前
基于百度地图JSAPI Three的城市公交客流可视化(三)——实时公交
前端·javascript·vue.js
晴殇i11 小时前
尤雨溪创立的 VoidZero 完成 1250 万美元 A 轮融资,加速整合前端工具链生态
前端·vue.js
菜市口的跳脚长颌11 小时前
一个 Vite 打包配置,引发的问题—— global: 'globalThis'
前端·vue.js·vite
胖虎26511 小时前
实现无缝滚动无滚动条的 Element UI 表格(附完整代码)
前端·vue.js
VOLUN12 小时前
Vue3 选择弹窗工厂函数:高效构建可复用数据选择组件
前端·javascript·vue.js
VOLUN12 小时前
Vue3 中 watch 第三个参数怎么用?6 大配置属性 + 场景指南
前端·javascript·vue.js