之前想用粒子来实现一下飞线的效果,看到很多大佬的代码发现使用粒子会是一个不错的选择,因为粒子的渲染比较省性能,之前看到有人使用圆形的粒子 后来发现其实普通的正方形的粒子就行,因为在线的粗度比较小的情况下,是看不太出来是圆形还是方形,下面为最终效果。
当然你可以在codepen上面查看在线演示以及代码。
总的来说,粒子是一个个的小小的点,而点是线的基本构成单位,只要在一个路径上生成比较多的点,然后将他们从开始的地方比较大到最后很小依次排列,就变成了前端大末端细的线条,同时动态更改材质输入的时间,让其在顶点着色器中体现出这个点的大小,就能看到他在动的效果了。而其实他的几何体并没有动,改变的只是不同时间下的不同位置的点的大小而已。
最开始的一步, 需要生成路径,在这个例子中路径是在不同的状态下旋转的椭圆路径
function initCircleCurveGroup(number){
let curves = [];
for (let i = 0; i < number; i++){
let curve = new THREE.EllipseCurve(
0, 0,
Math.random()*20+5, Math.random()*20+5,
0, 2 * Math.PI,
false,
0
);
curves.push(curve);
}
return curves;
}
上面的方法输入一个数字,这个数字为需要生成的路径数量, 所有的椭圆路径短半径和长半径都在5~25之间,所以中间有一小块会被空出来。
材质部分是比较重要的一点,其中顶点着色器是比较关键的部分
function initLineMaterial(setting){
let number = setting ? (Number(setting.number) || 1.0) : 1.0; // 在一个路径中同时存在的个数
let speed = setting ? (Number(setting.speed) || 1.0) : 1.0;// 速度约大越快
let length = setting ? (Number(setting.length) || 0.5) : 0.5;// 单根线的长度0-1之间1代表全满
let size = setting ?(Number(setting.size) || 3.0) : 3.0;// 在最大的地方的大小 默认为3像素
let color = setting ? setting.color || new THREE.Vector3(0,1,1) : new THREE.Vector3(0,1,1);// 颜色此处以Vector3的方式传入分别为RBG值 都是0-1的范围
let singleUniforms = {
u_time: commonUniforms.u_time,
number: {type: 'f', value:number},
speed: {type:'f',value:speed},
length: {type: 'f', value: length},
size: {type: 'f', value: size},
color: {type: 'v3', value: color}
};
let lineMaterial = new THREE.ShaderMaterial({
uniforms:singleUniforms,
vertexShader:document.getElementById('vertexShader').textContent,//顶点着色器部分
fragmentShader:document.getElementById('fragmentShader').textContent,// 片元着色器部分
transparent:true,
//blending:THREE.AdditiveBlending,
});
return lineMaterial;
}
以上的方法会根据配置生成一个自定义的shader材质。
commonUniforms.u_time 是我在全局中同一的一个时间变量 当然这个时间变量也可以是不同材质拥有自己的时间。
varying vec2 vUv;
attribute float percent;
varying float opacity;
uniform float u_time;
uniform float number;
uniform float speed;
uniform float length;
uniform float size;
void main()
{
vUv = uv;
vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
float l = clamp(1.0-length,0.0,1.0);//空白部分长度占比
gl_PointSize = clamp(fract(percent*number + l - u_time*number*speed)-l ,0.0,1.) * size * (1./length);
opacity = gl_PointSize/size;
gl_Position = projectionMatrix * mvPosition;
}
虽然顶点着色器的代码部分比较少,但是却是最为重要的部分,在此处我们专注于计算单个的点的大小。
首先percent代表的是该顶点在整个路径中的位置, 数值在0-1之间,0代表起点的位置 1代表终点的位置。
fract函数将整个的内容夹在0-1之间,相当于是取小数的部分。
l为空白部分的长度占比,在fract函数内部+l为的是让整个函数向前偏移空白的位置,这样线在最开始时是在起始位置,而在外面-l是因为让整个函数向下平移l个单位,这样在整个函数
-片元着色器
#ifdef GL_ES
#ifdef GL_ES
precision mediump float;
#endif
varying float opacity;
uniform vec3 color;
void main(){
if(opacity <=0.2){
discard;
}
gl_FragColor = vec4(color,1.0);
}
这块比较简单,鉴于点的显示机制,即便点的大小为0 ,点仍旧会被渲染,所以我们将实际像素大小0.2一下的内容统统不渲染。
到目前为止已经有生成一个椭圆路径的函数 以及生成线的材质的函数。下面的内容比较开放 可以选择自己喜欢的方式
// 根据curve和颜色 生成线条
/**
* @param curve {THREE.Curve} 路径,
* @param matSetting {Object} 材质配置项
* @param pointsNumber {Number} 点的个数 越多越细致
* */
function initFlyLine(curve,matSetting, pointsNumber){
var points = curve.getPoints( pointsNumber );
var geometry = new THREE.BufferGeometry().setFromPoints( points );
let length = points.length;
var percents = new Float32Array(length);
for (let i = 0; i < points.length; i+=1){
percents[i] = (i/length);
}
geometry.addAttribute('percent', new THREE.BufferAttribute(percents,1));
let lineMaterial = initLineMaterial(matSetting);
var flyLine = new THREE.Points( geometry, lineMaterial );
let euler = new THREE.Euler(Math.random()*Math.PI, Math.random()*Math.PI,0);
flyLine.setRotationFromEuler(euler);
scene.add(flyLine);
}
这个函数会生成一条线并且会旋转随机的角度。
function initCircleCurveGroup(number){
let curves = [];
for (let i = 0; i < number; i++){
let curve = new THREE.EllipseCurve(
0, 0,
Math.random()*20+5, Math.random()*20+5,
0, 2 * Math.PI,
false,
0
);
curves.push(curve);
}
return curves;
}
以上函数将会生成多个随机半径在5-20之间的椭圆路径 长宽可能是不一样的
function randomVec3Color(){
return new THREE.Vector3(
Math.random()*0.6 + 0.4,
Math.random()*0.6 + 0.4,
Math.random()*0.6 + 0.4
)
}
红绿蓝的通道都在0.4-1之间,稍微白一些的随机颜色;
let curves = initCircleCurveGroup(500);
for (let curve of curves){
initFlyLine(curve,{
speed: Math.random()*0.3+0.5,
number: Math.floor(Math.random()*9+1),
color: randomVec3Color(),
size:4.0
},2000)
}
将其放在渲染的前面
function render() {
commonUniforms.u_time.value +=0.01;
renderer.render( scene, camera );
}
在渲染的时候将共用的时间uniform+=0.1; 如果你需要真实时间截则需要自己设定计时器获得,这里使用每一帧的时间。