threejs 简单的点到点闪电或电流效果

文章目录

  • 效果
  • 一、实现原理
  • 二、实现步骤
    • 1.根据起点和终点计算对齐到X轴的矩阵
    • 2.生成基础拐点
    • 3、生成Curve并采样点
    • 4、将采样点进行小范围抖动偏移
    • 5、变换回原始位置
    • 6、增加逐帧显示的动效
    • 请添加图片描述
  • 核心代码


效果


threejs中简单的点到点闪电效果,也可以作为电流效果


一、实现原理

1、将起点和终点连线对齐到X轴上并且起点与原点重合,方便后续计算
2、在起点和终点之间均匀的获取数个点(如5个)作为基础的拐点
3、将上一个步骤获取的点生成一条Curve,在Curve上采样点,如50个点
4、将上一步骤采样的点在小范围内抖动,形成不规则弯曲的效果
5、加上动态效果

二、实现步骤

1.根据起点和终点计算对齐到X轴的矩阵

代码如下:

  /**
   * 获取3D空间任意两个点连接成直线,把直线变换到与X轴重合(起点与原点重合)的矩阵
   * @param {*} p1
   * @param {*} p2
   */
  getMatrixAlignToX(p1, p2) {
    // 1. 平移矩阵:将起点移至原点
    const T = new THREE.Matrix4().makeTranslation(-p1.x, -p1.y, -p1.z);
    // 2. 计算方向向量并归一化
    const V = new THREE.Vector3().subVectors(p2, p1);

    const u = V.clone().normalize();
    // 3. 构造旋转矩阵:对齐到X轴
    const q = new THREE.Quaternion().setFromUnitVectors(
      u,
      new THREE.Vector3(1, 0, 0)
    );
    const R = new THREE.Matrix4().makeRotationFromQuaternion(q);
    // 4. 组合变换矩阵
    const M = new THREE.Matrix4().multiplyMatrices(R, T);

    return M;
  }

代码中已经有比较详细的注释说明,这里再简单补充下:此矩阵计算就是组合了平移矩阵和旋转矩阵,平移矩阵用于将起点平移至原点,旋转矩阵用于将起点与终点的直线旋转到对齐X轴。获取到此矩阵后就可以将起点和终点乘上此矩阵,方便后面的计算。效果如下:黄色为变换前的起点和终点,蓝色为变换后的起点和终点。为了便于观察,已将z轴设置为0
threejs 简单的点到点闪电或电流效果_第1张图片

2.生成基础拐点

代码如下:

    const matrix = this.getMatrixAlignToX(startPos, endPos);
	const transformedStart = startPos.clone().applyMatrix4(matrix); // 变换起点
    const transformedEnd = endPos.clone().applyMatrix4(matrix); // 变换终点
    // 使用线性插值沿直线生成n个中间点:
    const points = [];
    const offset = 1 / count;
    for (let i = 0; i <= 1; i += offset) {
      // 5个均匀分布点
      const pos = new THREE.Vector3().lerpVectors(
        transformedStart,
        transformedEnd,
        i
      );
      points.push(pos);
    }

    // 除了首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过radius
    for (let i = 1; i < points.length - 1; i++) {
      const yOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
      const zOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
      points[i].y += yOffset;
      points[i].z += zOffset;
    }

先使用Vector3的lerpVectors函数在起点和终点之间均匀获取数个点,然后给除了起点和终点之外的点进行y轴和z轴的偏移(此时已对齐到X轴,计算很方便),要使闪电有随机性,这里用Math.random()计算偏移,效果如下

3、生成Curve并采样点

代码如下:

	// 生成平滑的曲线
    const curve = new THREE.CatmullRomCurve3(
      points, // 控制点数组
      false, // 是否闭合(默认false)
      "centripetal", // 曲线类型(可选:'centripetal'、'chordal'、'catmullrom')
    );

    // 平滑采样点
    const pointsOnCurve = curve.getPoints(divisions);

此时将采样点可视化出来就是上面视频的效果

4、将采样点进行小范围抖动偏移

代码如下:

	const transformPoints = [].concat(pointsOnCurve); // 复制数组
    // 对采样出的点除首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过threshold。形成抖动的闪电效果
    for (let i = 1; i < transformPoints.length - 1; i++) {
      const yOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
      const zOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
      transformPoints[i].y += yOffset;
      transformPoints[i].z += zOffset;
    }

将采样点可视化出现如下视频效果,可以通过调整第2步中拐点生成的偏移参数radius和第4步中采样点偏移参数threshold来设置生成的弯曲程度

5、变换回原始位置

前面为了计算方便,将起点和终点对齐到X轴,此时已经生成了闪电,需要乘上变换矩阵的逆矩阵变换回去

this.line.applyMatrix4(matrix.invert());

threejs 简单的点到点闪电或电流效果_第2张图片

6、增加逐帧显示的动效

原理就是使用setInterval函数每隔一段事件往line里增加几个顶点。同时增加闪电刷新

{
    ...
	const geometry = new LineGeometry();
    geometry.setPositions([
      this.allPoints[0].x,
      this.allPoints[0].y,
      this.allPoints[0].z,
    ]);

    if (!this.matLine)
      this.matLine = new LineMaterial({
        color: "#" + this._color,
        linewidth: this._lineWidth,
        dashed: false,
        transparent: true,
      });
    else {
      this.matLine.opacity = 1;
      this.matLine.visible = true;
    }

    this.line = new Line2(geometry, this.matLine);

    // 应用逆矩阵,将曲线变换回原始坐标系
    this.line.applyMatrix4(matrix.invert());
    this.obj.add(this.line);

	// 创建定时器逐步添加点
    this.pointAddInterval = setInterval(() => {
      this.addNextPoint();
    }, 10);

 	this.time = Math.max(this.showMinTime, Math.random() * this.showMaxTime); // 重置时间,随机一个起始点
    this.updateLineTimer = setTimeout(() => {
      this.updateLine();
    }, this.time);
}

/**
* 逐帧显示顶点
*/
addNextPoint() {
    if (this.currentPointIndex + 3 < this.allPoints.length) {
      this.currentPointIndex += 3;

      const newPositions = [];
      for (var i = 0; i < this.currentPointIndex; i++) {
        newPositions.push(
          this.allPoints[i].x,
          this.allPoints[i].y,
          this.allPoints[i].z
        ); 
      }
      this.line.geometry.dispose();
      this.line.geometry = new LineGeometry();
      this.line.geometry.setPositions(newPositions);
    } else {
      if (this.currentPointIndex < this.allPoints.length) {
        this.currentPointIndex = this.allPoints.length;
        const newPositions = [];
        for (var i = 0; i < this.currentPointIndex; i++) {
          newPositions.push(
            this.allPoints[i].x,
            this.allPoints[i].y,
            this.allPoints[i].z
          ); 
        }
        this.line.geometry.dispose();
        this.line.geometry = new LineGeometry();
        this.line.geometry.setPositions(newPositions);
      }

      clearInterval(this.pointAddInterval); // 停止添加点
    }
  }

/**
*  每隔一定时间隐藏闪电或重新绘制闪电
*/
updateLine() {
    if (this.line) {
      if (this.matLine.visible) {
        this.opacityInterval = setInterval(() => {
          this.fadeOpacity();
        }, 10);
        this.time = Math.max(
          this.hideMinTime,
          Math.random() * this.hideMaxTime
        ); // 重置时间,随机一个起始点
        if (this.updateLineTimer) clearTimeout(this.updateLineTimer);
        this.updateLineTimer = setTimeout(() => {
          this.updateLine();
        }, this.time);
      } else {
        if (this.opacityInterval) clearInterval(this.opacityInterval);
        this.opacityInterval = null;
        this.createCurve(); // 重新生成曲线
      }
    }
  }

threejs 简单的点到点闪电或电流效果_第3张图片

核心代码

注意需要引用Line2 类型,这种类型的线才能控制宽度

import { LineMaterial } from "../../three/examples/jsm/lines/LineMaterial.js";
import { LineGeometry } from "../../three/examples/jsm/lines/LineGeometry.js";
import { Line2 } from "../../three/examples/jsm/lines/Line2.js";

createCurve() {
    if (this.line) {
      if (this.updateLineTimer) clearTimeout(this.updateLineTimer);
      this.obj.remove(this.line);
      this.line.geometry.dispose();
      this.line = null;
    }

    // 添加当前绘制点索引
    this.currentPointIndex = 0;
    let res = this.generatePathPoints();
    const matrix = res.matrix; // 保存变换矩阵
    this.allPoints = res.points;
    this.allDir = [];
    for (var i = 0; i < this.allPoints.length; i++) {
      this.allDir.push(
        new THREE.Vector3()
          .random()
          .subScalar(0.5)
          .normalize()
      );
    }

    const geometry = new LineGeometry();
    geometry.setPositions([
      this.allPoints[0].x,
      this.allPoints[0].y,
      this.allPoints[0].z,
    ]);

    if (!this.matLine)
      this.matLine = new LineMaterial({
        color: "#" + this._color,
        linewidth: this._lineWidth,
        dashed: false,
        transparent: true,
      });
    else {
      this.matLine.opacity = 1;
      this.matLine.visible = true;
    }

    this.line = new Line2(geometry, this.matLine);

    // 应用逆矩阵,将曲线变换回原始坐标系
    this.line.applyMatrix4(matrix.invert());
    this.obj.add(this.line);

    // 创建定时器逐步添加点
    this.pointAddInterval = setInterval(() => {
      this.addNextPoint();
    }, 10); // 每50ms添加一个点(约20帧/秒)

    this.time = Math.max(this.showMinTime, Math.random() * this.showMaxTime); // 重置时间,随机一个起始点
    this.updateLineTimer = setTimeout(() => {
      this.updateLine();
    }, this.time);
  }

  addNextPoint() {
    if (this.currentPointIndex + 3 < this.allPoints.length) {
      this.currentPointIndex += 3;

      const newPositions = [];
      for (var i = 0; i < this.currentPointIndex; i++) {
        newPositions.push(
          this.allPoints[i].x,
          this.allPoints[i].y,
          this.allPoints[i].z
        ); // x坐标
      }
      this.line.geometry.dispose();
      this.line.geometry = new LineGeometry();
      this.line.geometry.setPositions(newPositions);
    } else {
      if (this.currentPointIndex < this.allPoints.length) {
        this.currentPointIndex = this.allPoints.length;
        const newPositions = [];
        for (var i = 0; i < this.currentPointIndex; i++) {
          newPositions.push(
            this.allPoints[i].x,
            this.allPoints[i].y,
            this.allPoints[i].z
          ); // x坐标
        }
        this.line.geometry.dispose();
        this.line.geometry = new LineGeometry();
        this.line.geometry.setPositions(newPositions);
      }

      clearInterval(this.pointAddInterval); // 停止添加点
    }
  }

  // 将原有的路径生成逻辑提取为独立方法
  generatePathPoints() {
    const startPos = this._startPos; // 起始点
    const endPos = this._endPos; // 终点
    const count = this._count; // 中间点数量
    const radius = this._radius; // 最大偏移半径
    const divisions = this._divisions; // 曲线采样点数
    const threshold = this._threshold; // 抖动范围

    // 将起点与终点变换到与X轴重合的坐标系下方便计算
    const matrix = this.getMatrixAlignToX(startPos, endPos);

    const transformedStart = startPos.clone().applyMatrix4(matrix); // 应变为 (0, 0, 0)
    const transformedEnd = endPos.clone().applyMatrix4(matrix); // 应变为 (||V||, 0, 0)

    // 使用线性插值沿直线生成n个中间点:
    const points = [];
    const offset = 1 / count;
    for (let i = 0; i <= 1; i += offset) {
      // 5个均匀分布点
      const pos = new THREE.Vector3().lerpVectors(
        transformedStart,
        transformedEnd,
        i
      );
      points.push(pos);
    }

    // 除了首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过radius
    for (let i = 1; i < points.length - 1; i++) {
      const yOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
      const zOffset = Math.random() * radius - radius / 2; // -r/2 到 r/2
      points[i].y += yOffset;
      points[i].z += zOffset;
    }

    // 生成平滑的曲线
    const curve = new THREE.CatmullRomCurve3(
      points, // 控制点数组
      false, // 是否闭合(默认false)
      "centripetal" // 曲线类型(可选:'centripetal'、'chordal'、'catmullrom')
    );

    // 平滑采样点
    const pointsOnCurve = curve.getPoints(divisions);

    // 查看采样出的点
    if (false) {
      const geometryTemp = new THREE.BufferGeometry().setFromPoints(
        pointsOnCurve
      );
      const materialTemp = new THREE.LineBasicMaterial({ color: 0x00ffff });
      const curveLineTemp = new THREE.Line(geometryTemp, materialTemp);
      // 应用逆矩阵,将曲线变换回原始坐标系
      //curveLineTemp.applyMatrix4(matrix.invert());
      this.obj.add(curveLineTemp);
    }

    const transformPoints = [].concat(pointsOnCurve);
    // 对采样出的点除首尾点外,每个点在Y轴和Z轴上都有一个随机偏移,最大不超过threshold。形成抖动的闪电效果
    for (let i = 1; i < transformPoints.length - 1; i++) {
      const yOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
      const zOffset = Math.random() * threshold - threshold / 2; // -r/2 到 r/2
      transformPoints[i].y += yOffset;
      transformPoints[i].z += zOffset;
    }

    const positions = new Float32Array(transformPoints.length * 3);
    for (let i = 0; i < transformPoints.length; i++) {
      positions[i * 3] = transformPoints[i].x;
      positions[i * 3 + 1] = transformPoints[i].y;
      positions[i * 3 + 2] = transformPoints[i].z;
    }

    return { points: transformPoints, matrix: matrix, positions: positions };
  }
  updateLine() {
    if (this.line) {
      if (this.matLine.visible) {
        this.opacityInterval = setInterval(() => {
          this.fadeOpacity();
        }, 10);
        this.time = Math.max(
          this.hideMinTime,
          Math.random() * this.hideMaxTime
        ); // 重置时间,随机一个起始点
        if (this.updateLineTimer) clearTimeout(this.updateLineTimer);
        this.updateLineTimer = setTimeout(() => {
          this.updateLine();
        }, this.time);
      } else {
        if (this.opacityInterval) clearInterval(this.opacityInterval);
        this.opacityInterval = null;
        this.createCurve(); // 重新生成曲线
      }
    }
  }

  fadeOpacity() {
    if (this.matLine && this.matLine.opacity > 0) {
      this.matLine.opacity -= 0.05;
    } else {
      this.matLine.visible = false;
      clearInterval(this.opacityInterval); // 停止添加点
      this.opacityInterval = null;
    }
  }

  /**
   * 获取3D空间任意两个点连接成直线,把直线变换到与X轴重合(起点与原点重合)的矩阵
   * @param {*} p1
   * @param {*} p2
   */
  getMatrixAlignToX(p1, p2) {
    // 1. 平移矩阵:将起点移至原点
    const T = new THREE.Matrix4().makeTranslation(-p1.x, -p1.y, -p1.z);
    // 2. 计算方向向量并归一化
    const V = new THREE.Vector3().subVectors(p2, p1);

    const u = V.clone().normalize();
    // 3. 构造旋转矩阵:对齐到X轴
    const q = new THREE.Quaternion().setFromUnitVectors(
      u,
      new THREE.Vector3(1, 0, 0)
    );
    const R = new THREE.Matrix4().makeRotationFromQuaternion(q);
    // 4. 组合变换矩阵
    const M = new THREE.Matrix4().multiplyMatrices(R, T);

    return M;
  }

你可能感兴趣的:(three.js)