
[UIデザイナーが学ぶ]Three.jsのパーティクルの基本的な話
普段はメーカーで冴えない中年UIデザイナーとして勤務しております。
あまり業務では利用しませんが、Three.jsのパーティクルについて学び直しているので、備忘録的にまとめています。
SphereGeometryの頂点情報を利用してパーティクルを表示
あまり利用するシーンはないかもしれませんが、既存のGeometryの頂点情報を利用してパーティクルを表示させます。
const particlesGeometry = new THREE.SphereGeometry(1, 32, 32);
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
sizeAttenuation: true, // falseにすると距離にかかわらずポイントの大きさが一定になる
});
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);

BufferGeometryを利用してパーティクルを表示
パーティクルを表示する場合は、BufferGeometryを利用することが多いと思います。
positionsはXYZの座標情報を格納します。

以下では、-5〜5の座標内にランダムにPointが配置されます。
const particlesGeometry = new THREE.BufferGeometry();
const count = 500;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 10;
}
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
sizeAttenuation: true,
});
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);

パーティクルにテクスチャを設定する
PointsMaterialにテクスチャを設定することができます。
テクスチャにする画像は以下です。colorプロパティを設定する前提で白いものにしました。

const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load("/textures/star.png")
const particlesGeometry = new THREE.BufferGeometry();
const count = 500;
const positions = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 10;
}
const particlesMaterial = new THREE.PointsMaterial({
size: 0.25, // あまり小さいとテクスチャがみえなくなるので調整
sizeAttenuation: true,
map: texture,
color: 0xff88ff, // 色も指定する
});
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);

透過されない問題
パーティクルにテクスチャを利用時にテクスチャが透過されない問題が発生します。
よく見ると、全面のPointが背面のPointを隠してしまっています。

これは、透過PNGをテクスチャとして利用しても同様の結果になります。
対策1 : アルファマップを利用する
アルファマップを利用することで、問題の一部を改善できます。

const particlesMaterial = new THREE.PointsMaterial({
size: 0.25,
sizeAttenuation: true,
map: particleTexture,
// mapで利用しているテクスチャが白黒なのでそのままアルファマップとして利用
alphaMap: particleTexture,
// transparentをtrueにする
transparent: true,
color: 0xff88ff,
});
ただし完全に解決することはできません。
以下のようにまだ透過できていない部分があります。
これはパーティクルが作成された順序と同じ順序で描画さる為、WebGL はどのパーティクルが他のパーティクルの前面にあるかを認識できない為におこります。

対策2 : depthWriteを利用する
depthWrite は、Three.js のマテリアルプロパティの一つで、オブジェクトが深度バッファに書き込むかどうかを制御します。depthWrite を true に設定すると、そのオブジェクトは深度バッファに書き込まれ、他のオブジェクトとの深度テストに使用されます。depthWrite を false に設定すると、そのオブジェクトは深度バッファに書き込まれず、深度テストに影響を与えません。
ちょっと難しいですが、とにかくこのプロパティをfalseにすることで、透過されない問題を解決することができます。

Pointの色を変更する
パーティクルの色をPoint事にランダムに変えてみます。
Pointの色を個別に変更する場合は、vertexColorsプロパティをtrueにします。
const particlesGeometry = new THREE.BufferGeometry();
const count = 500;
const positions = new Float32Array(count * 3);
const colors = new Float32Array(count * 3);
for (let i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 10;
colors[i] = Math.random(); // 追加
}
const particlesMaterial = new THREE.PointsMaterial({
size: 0.5,
sizeAttenuation: true,
map: particleTexture,
alphaMap: particleTexture,
transparent: true,
// color: 0xff88ff, // colorの影響をうけるのでコメントアウト
depthWrite: false,
vertexColors: true, // 追加
});
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(positions, 3)
);
// vertexColorsを設定
particlesGeometry.setAttribute(
"color",
new THREE.BufferAttribute(colors, 3)
);
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);

ブレンドモードを利用するとおもしろい表現になります。
ただし、プレンドはパフォーマンスに影響が大きいので注意です。
const particlesMaterial = new THREE.PointsMaterial({
size: 0.5,
sizeAttenuation: true,
map: particleTexture,
alphaMap: particleTexture,
transparent: true,
// color: 0xff88ff,
depthWrite: false,
vertexColors: true,
blending: THREE.AdditiveBlending, //追加
});
AdditiveBlending は、ピクセルの色を加算するブレンディングモードです。これにより、重なった部分がより明るくなり、光の効果や発光するオブジェクトを表現するのに適しています。

アニメーションさせる
geometryのattributes.positionを操作することでpoint事にアニメーションさせることができます。
以下2点考慮する必要があります。
属性を変更した後、Three.js にその変更を通知するために、needsUpdate フラグを true に設定する必要があります。
多くのパーティクルをアニメーションさせる場合、パフォーマンスに影響を与える可能性があります。可能であれば、GPU による計算を利用するためにシェーダーを使用することを検討してください。
const tick = () => {
const elapsedTime = clock.getElapsedTime();
for (let i = 0; i < count; i++) {
const i3 = i * 3;
const z = particlesGeometry.attributes.position.array[i3 + 2];
particlesGeometry.attributes.position.array[i3 + 0] +=
Math.sin(Math.PI * 0.1 * elapsedTime + z) * 0.005;
particlesGeometry.attributes.position.array[i3 + 1] +=
Math.cos(Math.PI * 0.1 * elapsedTime + z) * 0.005;
}
// レンダー前に行う
particlesGeometry.attributes.position.needsUpdate = true;
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();

パーティクルはやってみたくなる表現の一つだと思います。
カスタムシェーダーを使いこなしてもっとリッチな表現にチャレンジしたいです。