实现:使用粒子效果和自定义shader实现效果 (用图片应该更方便)
/*
* @Author: hongbin
* @Date: 2022-12-29 13:54:24
* @LastEditors: hongbin
* @LastEditTime: 2022-12-30 12:29:52
* @Description:流星
*/
import * as THREE from "three";
import { BufferAttribute, BufferGeometry } from "three";
import { MeteorShaderMaterial } from "./shader/material";
const defaultProps = {
numberOfPoints: 1000,
color: {
left: new THREE.Color("#5555ff"),
right: new THREE.Color("#00ffff"),
},
target: new THREE.Vector3(250, 20, 0),
};
type Props = typeof defaultProps;
export class MeteorClass {
group = new THREE.Group();
initial = {
length: 10,
deviation: 1,
};
target: Props["target"];
numberOfPoints: Props["numberOfPoints"];
color: Props["color"];
clock = new THREE.Clock();
t = 0;
second: number | undefined;
//不使用MeteorShaderMaterial 使用克隆体不改变原有材质 方便创建多个实例
material = MeteorShaderMaterial.clone();
hideSecond: number | undefined;
initSize = this.material.uniforms.size.value;
constructor(
/**
* 生成粒子数量
*/
numberOfPoints?: Props["numberOfPoints"],
/**
* 前进方向 如 x轴100 y轴20 new Vector3(100,20,0)
*/
target?: Props["target"],
/**
* 粒子渐变颜色
*/
color?: Props["color"],
/**
* 线段弧度级别
*/
radian?: number,
/**
* 多少秒播放完毕
*/
second?: number,
/**
* 流星轨迹消散时间
*/
hideSecond?: number,
/**
* 切断流星片段
*/
cut?: [number, number]
) {
this.numberOfPoints = numberOfPoints || defaultProps.numberOfPoints;
this.color = color || defaultProps.color;
this.target = target || defaultProps.target;
this.second = second;
this.hideSecond = hideSecond;
radian && (this.material.uniforms.radian.value = radian);
if (cut) {
this.material.uniforms.cutStart.value = cut[0];
this.material.uniforms.cutEnd.value = cut[1];
}
this.initObj();
}
genGeometry() {
const numberOfPoints = this.numberOfPoints;
const geometry = new BufferGeometry();
const positions = new Float32Array(numberOfPoints * 3);
const colors = new Float32Array(numberOfPoints * 3);
const percents = new Float32Array(numberOfPoints);
const { left: leftColor, right: rightColor } = this.color;
//! 不能用THREE.Color.sub减法函数 返回值最小0 影响生成正确颜色
const gradientColor = {
r: leftColor.r - rightColor.r,
g: leftColor.g - rightColor.g,
b: leftColor.b - rightColor.b,
};
/**
* 1最小 numberOfPoints最大
* 一条横着的渐渐变宽的粒子线条
*/
for (let i = 0, currLength = 0; i <= numberOfPoints; i++) {
const i3 = i * 3;
// const deviation = Math.random() * this.initial.deviation;
// const x = currLength + deviation;
// const x = deviation * 10;
const x = 0;
const y = 0;
const z = 0;
positions[i3] = x;
positions[i3 + 1] = y;
positions[i3 + 2] = z;
const percent = i / numberOfPoints;
percents[i] = percent;
colors[i3] = leftColor.r - gradientColor.r * percent;
colors[i3 + 1] = leftColor.g - gradientColor.g * percent;
colors[i3 + 2] = leftColor.b - gradientColor.b * percent;
currLength = (currLength + 1) % this.initial.length;
}
geometry.setAttribute("position", new BufferAttribute(positions, 3));
geometry.setAttribute("color", new BufferAttribute(colors, 3));
geometry.setAttribute("percent", new BufferAttribute(percents, 1));
return geometry;
}
initObj() {
const geometry = this.genGeometry();
this.material.uniforms.target = {
value: this.target,
};
const mesh = new THREE.Points(geometry, this.material);
this.group.add(mesh);
}
/**
* 更新进度
*/
update(p?: number) {
let percent = p!;
if (this.second) {
this.t += this.clock.getDelta();
percent = this.t / this.second;
if (percent >= 1 && this.hideSecond) {
let over = 1 - (this.t - this.second) / this.hideSecond;
if (over <= 0) {
this.t = 0;
over = 1;
percent = 0;
}
this.material.uniforms.size.value = this.initSize * over;
}
} else {
if (percent > 1) {
this.material.uniforms.size.value /= 1.1;
} else this.material.uniforms.size.value = this.initSize;
}
this.material.uniforms.progress.value = percent;
}
}
material
/*
* @Author: hongbin
* @Date: 2022-11-10 10:54:21
* @LastEditors: hongbin
* @LastEditTime: 2022-12-30 14:07:21
* @Description:自定义shader 材质
*/
import * as THREE from "three";
import vertexShader from "./vt.glsl";
import fragmentShader from "./fm.glsl";
export const MeteorShaderMaterial = new THREE.ShaderMaterial({
uniforms: {
time: { value: 0 },
//弥补自定义shader没有PointsMaterial材质的size属性
size: { value: 8 },
progress: { value: 0 },
color: { value: new THREE.Color("#0f00f0") },
/**
* 流星的弧度 0为直线
*/
radian: { value: 0 },
/**
* 切断开始
*/
cutStart: { value: 0 },
/**
* 切断结束
*/
cutEnd: { value: 0 },
},
blending: THREE.AdditiveBlending,
// side: 2,
transparent: true,
// blending: THREE.AdditiveBlending,
vertexShader,
//弥补自定义shader没有PointsMaterial材质的sizeAttenuation属性
fragmentShader,
// alphaTest: 0.001,
// depthTest: false,
depthWrite: false,
});
顶点着色器
varying vec3 vColor;
uniform float time;
uniform float size;
uniform float progress;
uniform vec3 target;
uniform float wave;
uniform float radian;
uniform float cutStart;
uniform float cutEnd;
attribute float percent;
attribute vec3 color;
void main() {
vec3 dispatchPos;
float p = min(progress, 1.);
float rp = p >= percent ? percent : p;
if(rp > cutStart && rp < cutEnd) {
rp = cutStart;
}
dispatchPos = position + (target - position) * rp;
dispatchPos.y *= sin(rp * 0.4) * radian;
vColor = color;
vec4 viewPosition = modelViewMatrix * vec4(dispatchPos, 1.0);
gl_Position = projectionMatrix * viewPosition;
gl_PointSize = size;
//最后进度等于1的点 放大
// gl_PointSize += step(1., rp) * 3.;
//近大远小效果 值自己调节
gl_PointSize *= (120. / -(modelViewMatrix * vec4(dispatchPos, 1.0)).z);
//后面细 前面粗
gl_PointSize *= (0.2 + rp);
// gl_PointSize *= (0.2 + (1. - rp));
}
片元着色器
// uniform vec3 color;
varying vec3 vColor;
void main() {
float strength = distance(gl_PointCoord, vec2(0.5));
strength = step(0.5, strength);
strength = 1.0 - strength;
gl_FragColor = vec4(vColor, strength);
// gl_FragColor = vec4(0.2431, 0.0039, 0.9608, 1.0);
}
使用
const Meteor = new MeteorClass(
1000,
new THREE.Vector3( 400,450, 0),
{
left: new THREE.Color("#f00"),
right: new THREE.Color("#00f"),
},
1,
3,
1
);
scene.add(Meteor.group);
Meteor.group.position.set(100 * Math.random(), 120 * random, 0);
Meteor.group.rotateZ(Math.PI / 0.9);
然后逐帧调用更新
Meteors.update();