Threejs烟花特效
粒子渲染
烟花的粒子渲染选择points + PointsMaterial + BufferGeometry。这里重点手动构建几何体的数据信息,包括点坐标,速度,颜色,重量。
pointsMaterial是渲染点的默认材质,如下使用
const material = new THREE.PointsMaterial({
size: 1, // 点大小
color: 0xffffff,
opacity: 1, // 透明度
vertexColors: true,
transparent: true,
blending: THREE.AdditiveBlending,
depthTest: false,
});
关键需要动态生成几何体的数据,代码j结构如下
const getPointMesh = (num, vels, type) => {
// geometry
const bufferGeometry = new THREE.BufferGeometry();
const vertices = [];
const velocities = [];
const colors = [];
const masses = [];
for (let i = 0; i < num; i++) {
const pos = new THREE.Vector3(0, 0, 0);
vertices.push(pos.x, pos.y, pos.z);
velocities.push(vels[i].x, vels[i].y, vels[i].z);
let size= Math.pow(vels[i].y, 2) * 0.04;
if (i === 0) size *= 1.1;
masses.push(size * 0.017);
colors.push(1.0, 1.0, 1.0, 1.0);
}
bufferGeometry.addAttribute('position', new THREE.Float32BufferAttribute(vertices, 3).setDynamic(true));
bufferGeometry.addAttribute('velocity', new THREE.Float32BufferAttribute(velocities, 3).setDynamic(true));
bufferGeometry.addAttribute('color', new THREE.Float32BufferAttribute(colors, 4).setDynamic(true));
bufferGeometry.addAttribute('mass', new THREE.Float32BufferAttribute(masses, 1).setDynamic(true));
const material = new THREE.PointsMaterial({
size: 1,
color: 0xffffff,
opacity: 1,
vertexColors: true,
transparent: true,
blending: THREE.AdditiveBlending,
depthTest: false,
});
return new THREE.Points(bufferGeometry, material);
};
生成bufferGeometry,循环生成position,循环次数表示生成的点的个数,每一个点的位置都是(0,0,0),每个点的位置相同,但是速度velocity不同,velocity数组由调用方传入,不同的调用方速度有不同的生成方法。颜色数组都是白色,重量数组每个值的大小设置为垂直速度分量成正比。
粒子更新
上面我们写了一个函数,接受数量和速度数组,可以创建出mesh返回,然后将该mesh加到场景中可以渲染出点,效果如下。因为所有点都在一个位置,所以只能看到一个点,同时为了方便查看下图将size设为100
接下来写状态更新部分逻辑,这里封装为Particle类,该类调用上面的函数获得实际渲染的mesh,类的update方法处理mesh中各个点位置的更新。
class ParticleMesh {
constructor(num, vels, type) {
this.particleNum = num;
this.timerStartFading = 10;
this.mesh = getPointMesh(num, vels, type);
}
disposeAll() {
this.mesh.geometry.dispose();
this.mesh.material.dispose();
}
update(gravity) {
if (this.timerStartFading > 0) this.timerStartFading -= 0.3;
const { position, velocity, color, mass } = this.mesh.geometry.attributes;
const decrementRandom = () => (Math.random() > 0.5 ? 0.98 : 0.96);
const decrementByVel = v => (Math.random() > 0.5 ? 0 : (1 - v) * 0.1);
for (let i = 0; i < this.particleNum; i++) {
const { x, y, z } = getOffsetXYZ(i);
velocity.array[y] += gravity.y - mass.array[i];
velocity.array[x] *= friction;
velocity.array[z] *= friction;
velocity.array[y] *= friction;
position.array[x] += velocity.array[x];
position.array[y] += velocity.array[y];
position.array[z] += velocity.array[z];
const { a } = getOffsetRGBA(i);
if (this.timerStartFading <= 0) {
color.array[a] *= decrementRandom() - decrementByVel(color.array[a]);
if (color.array[a] < 0.001) color.array[a] = 0;
}
}
position.needsUpdate = true;
velocity.needsUpdate = true;
color.needsUpdate = true;
}
}
update方法的逻辑是取出几何体的属性,遍历所有的粒子,在速度数组的每个位置上更新速度,然后使用速度更新位置,取出颜色分量,对透明度分量做衰减。需要注意两个辅助函数getOffsetXYZ和getOffsetRGBA,是转换粒子编号和数组下标的。
烟花粒子类
烟花分为升空部分和爆炸部分。两者都是粒子,区别在粒子的速度,前者是大致相同,向上发射;后者是圆形发散。烟花类是协调管理升空粒子和爆炸粒子的上层类,初始时创建一个升空粒子类,也就是生成一个方向向上的速度数组用来创建发射粒子,然后调用发射粒子的update方法,更新粒子状态;当粒子到顶后(速度衰减为0),删除发射粒子,创建爆炸粒子,也就是生成方向四周发散的速度数组,用来创建爆炸粒子,然后调用爆炸粒子的update方法更新。同时记录烟花寿命状态,当衰减到0,从场景中删除烟花。
//update伪代码
update(gravity) {
if (!this.isExploed) {
this.seed.update(gravity)
} else {
this.flower.update(gravity);
if (this.life > 0) this.life -= 1;
}
}
// 生成升空粒子
const num = 40;
const vels = [];
for (let i = 0; i < num; i++) {
const vx = 0;
const vy = i === 0 ? Math.random() * 2.5 + 0.9 : Math.random() * 2.0 + 0.4;
const vz = 0;
vels.push(new THREE.Vector3(vx, vy, vz));
}
// 生成速度,只有y方向有值,随机生成。
const pm = new ParticleSeedMesh(num, vels);
const x = Math.random() * 80 - 40;
const y = -50;
const z = Math.random() * 80 - 40;
pm.mesh.position.set(x, y, z);
// 设置mesh的位置,移动的发射初始位置
// 使用极坐标生成爆炸速度
for (let i = 0; i < num; i++) {
radius = getRandomNum(120, 60) * 0.01;
const theta = THREE.Math.degToRad(Math.random() * 180);
const phi = THREE.Math.degToRad(Math.random() * 360);
const vx = Math.sin(theta) * Math.cos(phi) * radius;
const vy = Math.sin(theta) * Math.sin(phi) * radius;
const vz = Math.cos(theta) * radius;
const vel = new THREE.Vector3(vx, vy, vz);
vel.multiplyScalar(this.flowerSizeRate);
vels.push(vel);
}
自定义shader
上面使用的是PointsMaterial,它画出来的点大小都是一样的,通过自定义shader可以控制每个点的大小能有区别
// vertex
precision mediump float;
attribute vec3 position;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform float size;
attribute float adjustSize;
uniform vec3 cameraPosition;
varying float distanceCamera;
attribute vec3 velocity;
attribute vec4 color;
varying vec4 vColor;
void main() {
vColor = color;
vec4 modelViewPosition = modelViewMatrix * vec4(position, 1.0);
gl_PointSize = size * adjustSize * (100.0 / length(modelViewPosition.xyz));
gl_Position = projectionMatrix * modelViewPosition;
}
通过gl_PointSize控制每个点的大小,adjustSize是随机生成的数组,让点大小有随机性,length(modelViewPosition.xyz)则是让点大小和相机距离成反比。
这样子点的大小就会有所区别。
现在的问题是视觉上方块太明显,这里的解决方案是通过纹理给定一个颜色乘积系数,让点更模糊一点。
生成纹理
const drawRadialGradation = (ctx, canvasRadius, canvasW, canvasH) => {
ctx.save();
const gradient = ctx.createRadialGradient(canvasRadius, canvasRadius, 0, canvasRadius, canvasRadius, canvasRadius);
gradient.addColorStop(0.0, 'rgba(255,255,255,1.0)');
gradient.addColorStop(0.5, 'rgba(255,255,255,0.5)');
gradient.addColorStop(1.0, 'rgba(255,255,255,0)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvasW, canvasH);
ctx.restore();
};
使用纹理
// fragement
precision mediump float;
uniform sampler2D texture;
varying vec4 vColor;
void main() {
vec4 color = vec4(texture2D(texture, gl_PointCoord));
gl_FragColor = color * vColor;
}
转载自:https://juejin.cn/post/7256010930017632293