前言
之前在研究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();
});
}
}
总结
实现飞线的三种方式,思路可借鉴在其它三维效果上
- 几何体实现
- 纹理贴图体实现
- 着色器实现