threejs多种方式封装飞线组件(几何、贴图、着色器)


前言

之前在研究threejs时,尝试了通过线几何体、通过纹理贴图、通过着色器等几种方式实现飞线效果,现在将这几种方式使用 typescript, 采用面向对象的方式封装整理并记录下来,思路供大家参考。


飞线实现效果

几何体实现

在这里插入图片描述


贴图实现

在这里插入图片描述


着色器实现
在这里插入图片描述

核心思路

几何体实现

  • 通过 TubeBufferGeometry 创建轨迹线模型
  • 采用相同的方式,从轨迹线的点集合中截取一段创建移动的飞线,通过 THREE.Line —— lerp 实现渐变色
  • 通过tween操作动画,每次重新设置线几何体的点坐标,实现动画效果

采用这种方式在思路上不难理解,但是需要使用者清楚 THREE.BufferGeometry 的使用。

纹理贴图体实现

  • 通过 THREE.CatmullRomCurve3 创建轨迹线,从上面截取点创建 THREE.Line 几何体作为轨迹线
  • 使用 THREE.TextureLoader 给模型表面贴图
  • 通过tween操作动画,每次操作纹理的 offset,实现动画

在网上找了个简单的纹理图,照理来说应该把它的背景色挖空:
在这里插入图片描述

着色器实现

着色器的实现相比复杂很多,主要指出关键部分:

创建点集

  • 仍然通过THREE.CatmullRomCurve3拾取点,数量为 n
  • setAttribute 给每个点传递一个索引属性,这个属性通过 attribute 变量传到着色器中,在实现飞线效果时有重要作用

创建着色器

  • 通过THREE.ShaderMaterial自定义着色器,通过tween向其传入 [0, n] 范围内不断变化的时间 uTime

点集的范围和时间循环的范围都是 [0, n]uTime 是不断变化的,给点索引值范围为 [uTime - m, uTime + m] 的粒子群们赋予着色器效果:粒子的大小由其索引值确定,索引值大的方向为头部,size更大 ; [uTime - m, uTime + m] 外的粒子们设置透明。随着uTime的不断变化,上述过程将会反复实现,形成飞线动画

代码实现

基类

import * as THREE from 'three';

interface flyLineBegin2End {
  begin: number[];
  end: number[];
  height: number;
}

export default abstract class FlyBase {
  // 必须声明的属性
  abstract scene: THREE.Scene // 场景
  abstract data: Array<flyLineBegin2End>; // 传入的数据
  abstract ThreeGroup: THREE.Group; // 存放实体

  // 实现的方法
  abstract _draw() : THREE.Group; // 添加场景
  abstract _remove() : void; // 移除实体
  abstract _animate() : void; // 开启动画
}

引入组件

  function createFlyLine() {
    const data =[
      { begin: [0, 0], end: [10, 0], height: 10 },
      { begin: [0, 0], end: [-20, 0], height: 10 },
      { begin: [0, 0], end: [15, 15], height: 10 },
    ]
    flyRef.current = new FlyLine(scene,data);
  }

飞线封装

  • 几何体

import * as THREE from 'three';
import FlyBase from './FlyBase'
import TWEEN from '@tweenjs/tween.js';

interface flyLineBegin2End {
  begin: number[];
  end: number[];
  height: number;
}

interface optionsInterface {
  routeColor: string;
  flyColor: string;
  cycle: number;
}

export default class FlyLine extends FlyBase {
  data: Array<flyLineBegin2End>;
  cycle: number;
  routeColor: string;
  flyColor: string;
  ThreeGroup: THREE.Group;
  scene: THREE.Scene 
  /**
   * 
   * @param data 数据配置
   * @param options 可选项
   */
  constructor(scene:THREE.Scene, data: Array<flyLineBegin2End>, options?: optionsInterface) {
    super()
    this.scene = scene;
    this.data = data;
    this.routeColor = options?.routeColor || '#00FFFF';
    this.flyColor = options?.flyColor || '#FFFF00';
    this.cycle = options?.cycle || 2000;
    this.ThreeGroup = new THREE.Group();
    
    scene.add(this._draw())
    this._animate()
  }

  _draw() {
    this.data.map((data)=>{
      const points = this._getPoints(data);
      const fixedLine = this._createFixedLine(points);
      const movedLine = this._createMovedLine(points, 10);

      this.ThreeGroup.add(fixedLine, movedLine);
      // 创建动画
      let tween = new TWEEN.Tween({ index: 0 })
        .to({ index: 100 }, this.cycle)
        .onUpdate(function (t) {
          let movedLineGeom = movedLine.geometry
          let id = Math.ceil(t.index);
          let pointsList = points.slice(id, id + 10); //从曲线上获取一段
          movedLineGeom && movedLineGeom.setFromPoints(pointsList);
          movedLineGeom.attributes.position.needsUpdate = true;
        })
        .repeat(Infinity);
     tween.start();
    })
    return this.ThreeGroup;
  }

  _animate() {
    TWEEN.update()
    requestAnimationFrame(()=>{this._animate()})
  }

  _getPoints(data: flyLineBegin2End) {
    const startPoint = data.begin; // 起始点
    const endPoint = data.end; // 终点
    const curveH = data.height; // 飞线最大高

    // 三点创建弧线几何体
    const pointInLine = [
      new THREE.Vector3(startPoint[0], 0, startPoint[0]),
      new THREE.Vector3(
        (startPoint[0] + endPoint[0]) / 2,
        curveH,
        (startPoint[1] + endPoint[1]) / 2,
      ),
      new THREE.Vector3(endPoint[0], 0, endPoint[1]),
    ];

    const curve = new THREE.CatmullRomCurve3(pointInLine);
    const points = curve.getSpacedPoints(100)

    return points
  }

  // 创建轨迹的线
  _createFixedLine(points: THREE.Vector3[]) {
    return new THREE.Line(
      new THREE.BufferGeometry().setFromPoints(points),
      new THREE.LineBasicMaterial({ color: this.routeColor })
    );
  }

  // 创建飞线
  _createMovedLine(points: THREE.Vector3[], length: number) {
    const pointsOnLine = points.slice(0, length); //从曲线上获取一段
    const flyLineGeom = new THREE.BufferGeometry();
    flyLineGeom.setFromPoints(pointsOnLine);

    // 操作颜色
    const colorArr: number[] = [];
    for (let i = 0; i < pointsOnLine.length; i++) {
      const color1 = new THREE.Color(this.routeColor); // 线颜色
      const color2 = new THREE.Color(this.flyColor); // 飞痕颜色
      // 飞痕渐变色
      let color = color1.lerp(color2, i / 5);
      colorArr.push(color.r, color.g, color.b);
    }
    // 设置几何体顶点颜色数据
    flyLineGeom.setAttribute( 'color', new THREE.BufferAttribute( new Float32Array(colorArr), 3 ));
    flyLineGeom.attributes.position.needsUpdate = true;

    const material = new THREE.LineBasicMaterial({
      vertexColors: true, //使用顶点本身颜色
    });

    return new THREE.Line(flyLineGeom, material);
  }
  // 修改显隐
  setVisible(visible: boolean) {
    this.ThreeGroup.visible = visible;
  }
  _remove() {
    this.scene.remove(this.ThreeGroup)
    this.ThreeGroup.children.map((l: any) => {
      l.geometry.dispose();
      l.material.dispose();
    });
  }
}
  • 纹理贴图
import * as THREE from "three";
import FlyBase from './FlyBase'
import TWEEN from '@tweenjs/tween.js';
import texture_img from '../../../assets/textures/arr.png'

interface flyLineBegin2End {
  begin: number[];
  end: number[];
  height: number;
}

interface optionsInterface {
  cycle: number;
}

export default class FlyLine extends FlyBase {
  scene: THREE.Scene 
  data: Array<flyLineBegin2End>;
  cycle: number;
  ThreeGroup: THREE.Group;

  constructor(scene:THREE.Scene, data: Array<flyLineBegin2End>, options?: optionsInterface) {
    super()
    this.scene = scene;
    this.data = data;
    this.ThreeGroup = new THREE.Group();
    this.cycle = options?.cycle || 2000;
    this.scene.add(this._draw())
    this._animate()
  }

  _animate() {
    TWEEN.update()
    requestAnimationFrame(() =>{ this._animate()})
  }

  _draw() {
    this.data.map((data)=>{
      const startPoint = data.begin; // 起始点
      const endPoint = data.end; // 终点
      const curveH = data.height; // 飞线最大高
  
      // 创建管道
      const pointInLine = [
        new THREE.Vector3(startPoint[0], 0, startPoint[0]),
        new THREE.Vector3(
          (startPoint[0] + endPoint[0]) / 2,
          curveH,
          (startPoint[1] + endPoint[1]) / 2,
        ),
        new THREE.Vector3(endPoint[0], 0, endPoint[1]),
      ];
  
      const lineCurve = new THREE.CatmullRomCurve3(pointInLine);
      const geometry = new THREE.TubeBufferGeometry(
        lineCurve, 100, 1, 2, false 
      );
  
      // 设置纹理
      const textloader = new THREE.TextureLoader();
      const texture = textloader.load(texture_img); //
      texture.repeat.set(5, 2);
      texture.needsUpdate = true
      texture.wrapS = THREE.RepeatWrapping;
      texture.wrapT = THREE.RepeatWrapping;
  
      const material = new THREE.MeshBasicMaterial({
        // color: 0xfff000,
        map: texture,
        transparent: true,
      });
  
      this.ThreeGroup.add(new THREE.Mesh(geometry, material));
  
      let tween = new TWEEN.Tween({ x:0 })
          .to({ x: 100 }, this.cycle)
          .onUpdate(function (t) {
            texture.offset.x -= 0.01
          })
          .repeat(Infinity);
       tween.start();
    })
    
    return this.ThreeGroup;
  }

  _remove() {
    this.scene.remove(this.ThreeGroup)
    this.ThreeGroup.children.map((l: any) => {
      l.geometry.dispose();
      l.material.dispose();
    });
  }
}

  • 着色器
import * as THREE from "three";
import FlyBase from './FlyBase'
import TWEEN from '@tweenjs/tween.js';

interface flyLineBegin2End {
  begin: number[];
  end: number[];
  height: number;
}

interface optionsInterface {
  routeColor: string;
  flyColor: string;
  cycle: number;
}

export default class FlyLine extends FlyBase {
  scene: THREE.Scene
  data: Array<flyLineBegin2End>;
  cycle: number;
  routeColor: string;
  flyColor: string;
  ThreeGroup: THREE.Group;

  constructor(scene: THREE.Scene, data: Array<flyLineBegin2End>, options?: optionsInterface) {
    super()
    this.scene = scene;
    this.data = data;
    this.ThreeGroup = new THREE.Group();
    this.cycle = options?.cycle || 2000;
    this.routeColor = options?.routeColor || '#00FFFF';
    this.flyColor = options?.flyColor || '#FFFF00';
    this.scene.add(this._draw())
    this._animate()
  }

  _animate() {
    TWEEN.update()
    requestAnimationFrame(() => { this._animate() })
  }

  _draw() {
    this.data.map((data) => {
      const startPoint = data.begin; // 起始点
      const endPoint = data.end; // 终点
      const curveH = data.height; // 飞线最大高

      const begin = new THREE.Vector3(startPoint[0], 0, startPoint[0])
      const end = new THREE.Vector3(endPoint[0], 0, endPoint[1])
      const len = begin.distanceTo(end);

      // 创建管道
      const pointInLine = [
        new THREE.Vector3(startPoint[0], 0, startPoint[0]),
        new THREE.Vector3(
          (startPoint[0] + endPoint[0]) / 2,
          curveH,
          (startPoint[1] + endPoint[1]) / 2,
        ),
        new THREE.Vector3(endPoint[0], 0, endPoint[1]),
      ];


      const lineCurve = new THREE.CatmullRomCurve3(pointInLine);

      const points = lineCurve.getPoints(1000);

      const indexList: number[] = [];
      const positionList: number[] = [];
      points.forEach((item, index) => {
        indexList.push(index)
      })

      const geometry = new THREE.BufferGeometry().setFromPoints(points);
      geometry.setAttribute('aIndex', new THREE.Float32BufferAttribute(indexList, 1))

      const material = new THREE.ShaderMaterial({
        uniforms: {
          uColor: {
            value: new THREE.Color(this.flyColor)
          },
          uTime: {
            value: 0,
          },
          uLength: {
            value: points.length,
          },
        },
        vertexShader: `
        attribute float aIndex;

        uniform float uTime;
        uniform vec3 uColor;

        varying float vSize;

        void main(){
            vec4 viewPosition = viewMatrix * modelMatrix *vec4(position,1);
            gl_Position = projectionMatrix * viewPosition;

            if(aIndex < uTime + 100.0 && aIndex > uTime - 100.0){
              vSize = (aIndex + 100.0 - uTime) / 60.0;
            } 
            gl_PointSize =vSize;
        }
      `,
        fragmentShader: `
        varying float vSize;
        uniform vec3 uColor;
        void main(){

            if(vSize<=0.0){
                gl_FragColor = vec4(1,0,0,0);
            }else{
                gl_FragColor = vec4(uColor,1);
            }
            
        }
      `,
        transparent: true,
      })

      this.ThreeGroup.add(new THREE.Points(geometry, material));

      let tween = new TWEEN.Tween({ index: 0 })
        .to({ index: 1000 }, this.cycle)
        .onUpdate(function (t) {
          let id = Math.ceil(t.index);
          material.uniforms.uTime.value = id
        })
        .repeat(Infinity);
      tween.start();
    })
    return this.ThreeGroup;
  }

  _remove() {
    this.scene.remove(this.ThreeGroup)
    this.ThreeGroup.children.map((l: any) => {
      l.geometry.dispose();
      l.material.dispose();
    });
  }
}

总结

实现飞线的三种方式,思路可借鉴在其它三维效果上

  • 几何体实现
  • 纹理贴图体实现
  • 着色器实现