パーティクルレンダリング
花火のパーティクル レンダリングの場合は、ポイント + ポイントマテリアル + バッファジオメトリを選択します。ここでの焦点は、ポイント座標、速度、色、重みなどのジオメトリのデータ情報を手動で構築することです。
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);
};
バッファジオメトリを生成します。位置を生成するループ。ループの数は生成されたポイントの数を示します。各ポイントの位置は (0,0,0) です。各ポイントの位置は同じですが、速度は異なります。配列は呼び出し元の入力によって渡され、呼び出し元の速度が異なれば生成方法も異なります。カラー配列はすべて白で、重み配列の各値のサイズは垂直速度成分に比例するように設定されます。
パーティクルの更新
上記では、数値と速度の配列を受け取り、メッシュを作成して返し、そのメッシュをシーンに追加してポイントをレンダリングできる関数を作成しました。その効果は次のとおりです。すべての点が 1 つの位置にあるため、1 つの点だけが表示されます。次の図を見やすくするために、サイズは 100 に設定されています。
次に、Particle クラスとしてカプセル化された状態更新ロジックを記述します。このクラスは上記の関数を呼び出して実際にレンダリングされたメッシュを取得し、クラスの update メソッドはメッシュ内の各ポイントの位置の更新を処理します。
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 メソッドのロジックは、ジオメトリのプロパティを取得し、すべてのパーティクルを走査し、速度配列の各位置での速度を更新し、次にその速度を使用して位置を更新し、カラー コンポーネントを取り出して、透明コンポーネント。2 つの補助関数 getOffsetXYZ と getOffsetRGBA はパーティクル番号と配列添字を変換することに注意してください。
花火の粒子
花火は打ち上げパートと爆発パートに分かれています。どちらも粒子であり、違いは粒子の速度にあり、前者はほぼ同じで上方へ放射しますが、後者は円形発散です。花火クラスは打ち上げパーティクルと爆発パーティクルを調整・管理する上位クラスで、最初に打ち上げパーティクルクラスを作成、つまり上向きの速度配列を生成して発射パーティクルを作成し、更新メソッドを作成します。放出されたパーティクルの は、パーティクルの状態を更新するために呼び出されます。; パーティクルが頂点に達すると (速度が 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);
}
カスタムシェーダ
上面使用的是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;
}