使用Three.js粒子系统打造梦幻银河效果(附完整代码解析)

1 效果预览

在这里插入图片描述
通过参数调节可实现不同形态的银河效果,支持实时交互和全屏查看

2 核心实现步骤

2.1 初始化Three.js基础环境

// 初始化场景
const scene = new THREE.Scene();

// 创建透视相机
const camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000);
camera.position.set(0, 0, 5);

// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
  canvas: webglCanvas.value,
  alpha: true,
  antialias: true
});

2.2 创建银河粒子系统

几何体生成逻辑:

const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);

for (let i = 0; i < parameters.count; i++) {
  // 极坐标计算粒子位置
  const radius = Math.random() * galaxyRadius;
  const spinAngle = radius * spinFactor;
  const branchAngle = (i % branches) / branches * Math.PI * 2;
  
  // 添加随机扰动
  positions[i*3] = Math.cos(branchAngle + spinAngle) * radius + randomX;
  positions[i*3+1] = randomY; 
  positions[i*3+2] = Math.sin(branchAngle + spinAngle) * radius + randomZ;
  
  // 颜色插值
  colors[i*3] = insideColor.r + (outsideColor.r - insideColor.r) * (radius / maxRadius);
}

材质配置:

const material = new THREE.PointsMaterial({
  size: particleSize,
  map: particleTexture, // 使用粒子贴图
  blending: THREE.AdditiveBlending,
  depthWrite: false,
  vertexColors: true
});

2.3 参数控制面板实现

const gui = new dat.GUI();

gui.add(parameters, 'branches', 2, 20).name('旋臂数量').onChange(updateGalaxy);
gui.addColor(parameters, 'insideColor').name('核心颜色').onChange(updateGalaxy);
gui.add(parameters, 'spin', -5, 5).name('旋转强度').onChange(updateGalaxy);

3 关键参数详解

参数名类型范围说明
countnumber1000-10000粒子总数
branchesnumber2-20银河旋臂数量
spinnumber-5~5旋转强度(正负值方向不同)
randomnessnumber0-2粒子位置随机扰动强度
randomPowernumber1-101-10 扰动分布曲线

4 性能优化技巧

4.1 对象复用

更新参数时先销毁旧几何体

if (oldPoints) {
  geometry.dispose();
  material.dispose();
  scene.remove(oldPoints);
}

4.2 渲染优化

  • 开启抗锯齿:antialias: true
  • 使用AdditiveBlending混合模式
  • 关闭深度写入:depthWrite: false

4.3 内存管理

  • 组件卸载时释放资源
  • 使用dispose()方法清理Three.js对象

5 完整代码

<!-- eslint-disable no-undef -->
<template>
  <div class="galaxy">
    <canvas ref="webglCanvas" class="webgl"></canvas>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import * as dat from "dat.gui"; // 导入dat.gui

export default defineComponent({
  name: "GalaxyView",
  setup() {
    const webglCanvas = ref<HTMLCanvasElement | null>(null);

    let scene: THREE.Scene;
    let camera: THREE.PerspectiveCamera;
    let renderer: THREE.WebGLRenderer;
    let controls: OrbitControls;
    const parameters: any = {};
    let gui: dat.GUI | null = null;
    const initThree = () => {
      if (!webglCanvas.value) return;
      /**
       * gui
       */
      gui = new dat.GUI();
      /**
       * 场景
       */
      scene = new THREE.Scene();

      /**
       * 银河
       */
      // 创建银河
      parameters.count = 3000;
      parameters.size = 0.01;
      parameters.insideColor = 0xff6030;
      parameters.outsideColor = 0x00ff00;
      parameters.radius = 5;
      parameters.branches = 3; //分支
      parameters.spin = 1; //旋转
      parameters.randomness = 0.5; //随机度
      parameters.randomPower = 3; //随机度的幂

      let geometry: THREE.BufferGeometry<THREE.NormalBufferAttributes> | null =
        null;
      let material: THREE.PointsMaterial | null = null;
      let Points: THREE.Object3D<THREE.Object3DEventMap> | null = null;

      const generateGalaxy = () => {
        /**
         * 销毁旧的
         */
        if (Points !== null && geometry !== null && material !== null) {
          //scene.remove(Points);
          geometry.dispose();
          material.dispose();
          scene.remove(Points);
        }
        /**
         * 几何体
         */
        geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(parameters.count * 3);
        const colors = new Float32Array(parameters.count * 3);

        const insideColor = new THREE.Color(parameters.insideColor);
        const outsideColor = new THREE.Color(parameters.outsideColor);

        for (let i = 0; i < parameters.count; i++) {
          const i3 = i * 3;
          /**
           * 银河的位置
           */
          const radius = Math.random() * parameters.radius;
          const spinAngle = radius * parameters.spin;
          //获得每个分支的角度
          const branchAngle =
            ((i % parameters.branches) / parameters.branches) * Math.PI * 2;

          //随机度
          const randomX =
            Math.pow(Math.random(), parameters.randomPower) *
            (Math.random() < 0.5 ? 1 : -1);
          const randomY =
            Math.pow(Math.random(), parameters.randomPower) *
            (Math.random() < 0.5 ? 1 : -1);
          const randomZ =
            Math.pow(Math.random(), parameters.randomPower) *
            (Math.random() < 0.5 ? 1 : -1);

          positions[i3 + 0] =
            Math.cos(branchAngle + spinAngle) * radius + randomX;
          positions[i3 + 1] = 0 + randomY;
          positions[i3 + 2] =
            Math.sin(branchAngle + spinAngle) * radius + randomZ;

          /**
           * 银河颜色
           */
          colors[i3 + 0] =
            outsideColor.r +
            (insideColor.r - outsideColor.r) * (1 - radius / parameters.radius);
          colors[i3 + 1] =
            outsideColor.g +
            (insideColor.g - outsideColor.g) * (1 - radius / parameters.radius);
          colors[i3 + 2] =
            outsideColor.b +
            (insideColor.b - outsideColor.b) * (1 - radius / parameters.radius);
        }

        geometry.setAttribute(
          "position",
          new THREE.BufferAttribute(positions, 3)
        );
        geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));

        /**
         * 材质
         */
        material = new THREE.PointsMaterial({
          size: parameters.size,
          sizeAttenuation: true, //是否衰减
          depthWrite: false, //是否渲染深度
          blending: THREE.AdditiveBlending, //混合模式
          vertexColors: true, //是否使用顶点颜色,
        });

        /**
         * 粒子
         */
        Points = new THREE.Points(geometry, material);
        Points.position.set(0, 0, 0);
        scene.add(Points);
      };
      generateGalaxy();

      gui
        .add(parameters, "count", 1000, 10000, 100)
        .name("粒子数量")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "size", 0.001, 0.1, 0.001)
        .name("粒子大小")
        .onFinishChange(generateGalaxy);
      gui
        .addColor(parameters, "insideColor")
        .name("内部颜色")
        .onFinishChange(generateGalaxy);
      gui
        .addColor(parameters, "outsideColor")
        .name("外部颜色")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "radius", 0.01, 20, 0.01)
        .name("银河半径")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "branches", 2, 20, 1)
        .name("银河分支数量")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "spin", -5, 5, 0.001)
        .name("银河旋转")
        .onChange(generateGalaxy);
      gui
        .add(parameters, "randomness", 0, 2, 0.001)
        .name("随机度")
        .onFinishChange(generateGalaxy);
      gui
        .add(parameters, "randomPower", 1, 10, 0.001)
        .name("随机度的幂")
        .onFinishChange(generateGalaxy);
      /**
       * 纹理
       */
      const textureLoader = new THREE.TextureLoader();
      const particleTexture1 = textureLoader.load("/particle1.png");

      /**
       * 相机
       */
      // 创建相机,调整宽高比
      const width = window.innerWidth;
      const height = window.innerHeight;
      camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
      camera.position.set(0, 0, 5);

      // 渲染器,使用 canvas 元素
      renderer = new THREE.WebGLRenderer({
        canvas: webglCanvas.value,
        alpha: true,
        antialias: true,
      });
      // 设置渲染器大小
      renderer.setSize(width, height);
      //设置像素比
      renderer.setPixelRatio(window.devicePixelRatio);

      // 添加控制面板
      controls = new OrbitControls(camera, webglCanvas.value); //webglCanvas.value与render.docutement一样
      controls.enablePan = false; //禁止拖动
      controls.enableDamping = true; //阻尼效果

      // 动画效果
      const animate = () => {
        requestAnimationFrame(animate);
        //更新粒子位置
        controls.update();
        renderer.render(scene, camera);
      };
      animate();
    };

    const onWindowResize = () => {
      if (camera && renderer) {
        const width = window.innerWidth;
        const height = window.innerHeight;
        camera.aspect = width / height;
        camera.updateProjectionMatrix();
        renderer.setSize(width, height);
        renderer.setPixelRatio(window.devicePixelRatio);
      }
    };
    //双击事件
    const ondblclick = () => {
      if (!document.fullscreenElement) {
        //有的浏览器不支持fullscreenElement,还还会有其他的,可以加判断,这就不加了。。
        webglCanvas.value?.requestFullscreen();
      } else {
        document.exitFullscreen();
      }
    };

    onMounted(() => {
      initThree();
      window.addEventListener("resize", onWindowResize);
      window.addEventListener("dblclick", ondblclick);
    });

    onUnmounted(() => {
      if (renderer) {
        renderer.dispose();
      }
      if (gui) {
        gui.destroy(); // 销毁 dat.gui
        gui = null;
      }
      window.removeEventListener("resize", onWindowResize);
    });

    return {
      webglCanvas,
    };
  },
});
</script>

<style scoped>
/* 这个地方的居中思想挺重要的,可以让元素在页面中居中显示。 */
.about {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  overflow: hidden;
  margin: 0;
}

.webgl {
  background-color: rgb(1, 1, 1);
  width: 100%;
  height: 100%;
  display: block;
  position: fixed;
  left: 0;
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小牛码495

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值