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();
    });
  }
}

总结

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

  • 几何体实现
  • 纹理贴图体实现
  • 着色器实现
### 使用 Three.js 实现 3D 地球上的线路效果 为了实现带有线路效果的3D地球,可以采用`three.js`库来构建场景,并利用其丰富的功能完成这一目标。下面是一个简单的示例教程。 #### 创建基本环境 首先初始化WebGL渲染器、设置相机视角以及建立场景: ```javascript // 初始化 renderer 渲染器 const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); // 设置 camera 相机位置 const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.z = 2; // 建立 scene 场景 const scene = new THREE.Scene(); // 添加光源以便观察物体颜色变化 scene.add(new THREE.AmbientLight(0xffffff)); ``` #### 构建3D地球模型 接着使用`THREE.SphereGeometry`创建一个球形几何体作为地球的基础形状,并应用纹理映射使其看起来更真实[^2]。 ```javascript // 加载 Earth 的材质图片 var textureLoader = new THREE.TextureLoader(); let earthTexture = textureLoader.load('path_to_earth_texture.jpg'); // 定义地球半径和分段数以提高平滑度 const geometry = new THREE.SphereGeometry(0.5, 32, 32); const material = new THREE.MeshBasicMaterial({ map: earthTexture }); const sphere = new THREE.Mesh(geometry, material); // 将地球加入到场景中 scene.add(sphere); ``` #### 绘制行路径 对于想要显示从一点到另一点之间的行路线,则可以通过计算两点间的大圆弧线(Great Circle Arc),然后沿着这条曲线放置多个小立方体或其他简单图形形成连续线条表示航线[^1]。 ```javascript function createFlightPath(startLatLon, endLatLon){ let pathPoints = []; function latLonToXYZ(lat, lon) { var phi = (90 - lat) * Math.PI / 180; var theta = (lon + 180) * Math.PI / 180; return [ 0.5 * Math.sin(phi) * Math.cos(theta), 0.5 * Math.sin(phi) * Math.sin(theta), 0.5 * Math.cos(phi) ]; } // 计算起点终点坐标转换成xyz坐标系下的值 const startCoord = latLonToXYZ(...startLatLon); const endCoord = latLonToXYZ(...endLatLon); // 插入中间过渡点构成完整的行轨迹 for(let i=0; i<=100; ++i){ let t = i/100; let pointX = startCoord[0]*(1-t)+endCoord[0]*t; let pointY = startCoord[1]*(1-t)+endCoord[1]*t; let pointZ = startCoord[2]*(1-t)+endCoord[2]*t; pathPoints.push([pointX, pointY, pointZ]); } // 根据这些点生成一系列的小方块模拟行路径 pathPoints.forEach((coords)=>{ let cubeGeom = new THREE.BoxGeometry(.01,.01,.01); let lineMat = new THREE.MeshBasicMaterial({color: 0xff0000}); let marker = new THREE.Mesh(cubeGeom,lineMat ); marker.position.set(coords[0], coords[1], coords[2]); scene.add(marker); }); } ``` 此函数接受两个参数分别为起始地点经纬度数组[startLatitude,startLongitude] 和结束地点经纬度[endLatitude,endLongitude] ,之后调用该方法即可在地球上画出一条红色的行路径。 #### 控制视图移动 为了让用户体验更加友好,在实际开发过程中还可以引入`FlyControls`插件让用户能够自由操控摄像头围绕着整个星球查看不同的角度[^4]。 ```javascript import { FlyControls } from 'https://cdn.jsdelivr.net/npm/three@0.122/examples/jsm/controls/FlyControls.js'; const controls = new FlyControls(camera, renderer.domElement); controls.movementSpeed = 1.0; controls.rollSpeed = Math.PI / 12; ``` 最后启动动画循环不断更新画面帧率保持流畅播放效果: ```javascript function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值