Three.js/シェーダー(GLSL)その2

目次
  1. ポストプロセッシング
  2. シェーダーを使ってパーティクルをアニメーションする
    1. sizeとsizeAttenuation
    2. パーティクルのサイズを高密度ディスプレイに対応させる
    3. パーティクルのサイズを画面の高さに比例させる
    4. パーティクルにパターンを適応
    5. modelPositionを動かすアニメーション
      1. 宇宙(実装例)
    6. パーティクルにテクスチャを使用
    7. パーティクルを球体上に分布させる
    8. GSAPを用いたアニメーション
      1. 花火(実装例)
  3. 既存のマテリアルを改変

ポストプロセッシング

ポストプロセッシングは最終的な画像(レンダリング)にエフェクトを加えることです。

  1. ポストプロセッシングでは、レンダーターゲットでレンダリングした「テクスチャ」が生成されます
  2. このレンダーターゲットから得られた「テクスチャ」は、カメラに向かって配置され、ビューポート全体をカバーする「プレーン(平面)」に適用されます
  3. この「プレーン(平面)」は、ポストプロセッシング効果を適用するための特別なフラグメントシェーダーを含むマテリアルを使用します

Three.jsでは、これらの「エフェクト」を「パス」と呼びます
「複数のパス」を組み合わせることで、複雑なエフェクトを作成でき、最終的なパスはキャンバスに直接描画され、ユーザーに最終結果が表示されます

  • レンダーターゲットは、画像やテクスチャを一時的に保持するための容器のようなものです
    WebGLやThree.jsでの3Dレンダリングでは、通常、画像は最終的にキャンバス描画されます
    しかし、ポストプロセッシングでは、直接キャンバスに描画する代わりに、一時的なレンダーターゲットに描画します
    これにより、追加の処理や効果を適用する前に画像を保存し、操作することができます
  • ピンポンバッファリングは、ポストプロセッシングの「複数のパス」を効果的に連携させるための手法です
    一つのレンダーターゲットに描画する間、もう一つのレンダーターゲットからテクスチャを取得し、そのテクスチャに対して処理を行います
    これにより、一連のポストプロセッシング効果(パス)を順番に適用できます
    レンダリングプロセス中に、一つのレンダーターゲットに描画しながら、そのレンダーターゲットのテクスチャを読み取ることはできないそのため、2つのレンダーターゲットが必要です

https://threejs.org/docs/#examples/en/postprocessing/EffectComposer

EffectComposer:ポストプロセッシングのプロセスを管理するためのクラスです
RenderPass:ポストプロセッシングチェーン内で最初に実行されるパスで、主な役割は、シーンとカメラを使って初期のレンダリングを行います
*EffectComposerはエフェクトの適用を制御、RenderPassは最初のレンダリングを提供
その出力を後続のポストプロセッシングパスに渡します
*インポート必要

DotScreenPassで画像にドットスクリーン効果を適用するエフェクトを適応

import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'
//画像にドットスクリーン効果を適用するエフェクト
import { DotScreenPass } from 'three/examples/jsm/postprocessing/DotScreenPass.js'

//Post processing
//EffectComposer
const effectComposer = new EffectComposer(renderer)
effectComposer.setSize(sizes.width, sizes.height)
effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
//RenderPass
const renderPass = new RenderPass(scene, camera)
effectComposer.addPass(renderPass)
//DotScreenPassを適応
const dotScreenPass = new DotScreenPass()
/*
dotScreenPass.enabled = false
でパスを無効にできる 
*/
effectComposer.addPass(dotScreenPass)

//アニメーションの関数内では
const アニメーション = () =>{
    // ...
    //置き換える
    // renderer.render(scene, camera)
    effectComposer.render()
    // ...
}

//EffectComposerとそのパスは、ウィンドウのサイズが変更されたときリサイズする

window.addEventListener('resize', () =>{
   // ...
      // EffectComposerの更新
    effectComposer.setSize(window.innerWidth, window.innerHeight);
    effectComposer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  // ...
})

GlitchPass
画面のグリッチ(乱れ)を模倣するエフェクト
デジタル的なエラー、歪み、乱れを表現し、映画やビデオゲームでよく見られるスタイル

import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js'
// ...
const glitchPass = new GlitchPass()
//glitchPass.goWild = true //ノンストップ 
effectComposer.addPass(glitchPass)

UnrealBloomPass
ブルーム効果(光の輝き)は、光源や反射面からの光が周囲に広がり、光沢感や輝きを演出
strength強度: ブルームの強度を指定します。
radius半径: ブルームの広がり具合を指定します。
thresholdしきい値: ブルームを適用する明るさのしきい値を指定します

import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'

// ...
const unrealBloomPass = new UnrealBloomPass()
unrealBloomPass.strength = 0.5
unrealBloomPass.radius = 0.5
unrealBloomPass.threshold = 0.5
effectComposer.addPass(unrealBloomPass)

RenderPixelatedPass
特定のピクセルサイズでレンダリング

import { RenderPixelatedPass } from 'three/addons/postprocessing/RenderPixelatedPass.js';

// ...
const renderPixelatedPass = new RenderPixelatedPass( 10, scene, camera );
effectComposer.addPass( renderPixelatedPass );

ポストプロセッシングを適用した後、レンダリングされたシーンが暗くなってしまうことがあります
*レンダーターゲットでレンダリングされるため、色空間が同じ方法でサポートされない
解決策:GammaCorrectionShaderを最後のパスとして追加する
このシェーダーは色空間をSRGBに変換し、色を補正する

import { GammaCorrectionShader } from 'three/examples/jsm/shaders/GammaCorrectionShader.js';

// ...

const gammaCorrectionPass = new ShaderPass(GammaCorrectionShader);
effectComposer.addPass(gammaCorrectionPass);

アンチエイリアシング:エッジがジャギー(刻み目のようになる現象)に見える問題を解決する
*高品質ほどパフォーマンスは悪くなります
注意:GammaCorrectionShaderより後に設定する(エフェクトが色空間の調整後の画像に対して最適な設定をするため)

  • SSAA :最高品質のアンチエイリアシング
  • SMAA:FXAAよりも高品質
    エッジ検出と局所的な処理により、より洗練されたエッジを提供します
  • TAA:複数のフレームを統合してエッジのジャギーを減らします
    結果は比較的良いが、動きのあるシーンではボケやゴースト現象が生じることがる
  • FXAA :場合によっては少しボケた印象になります。
    画像全体にフィルタを適用することでエッジを滑らかにする
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';

// ...

const smaaPass = new SMAAPass();
effectComposer.addPass(smaaPass);

RGBShiftエフェクト
赤(R)緑(G)青(B)の色成分をそれぞれ少しずつずらすことで、特有の視覚的な歪みを生み出すエフェクト
適用手順
RGBShiftShader:RGBShiftエフェクトのためのシェーダー
ShaderPass:このシェーダーをポストプロセッシングパイプラインに統合するために使用

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'
import { RGBShiftShader } from 'three/examples/jsm/shaders/RGBShiftShader.js'

// ...

const rgbShiftPass = new ShaderPass(RGBShiftShader)
effectComposer.addPass(rgbShiftPass)

独自のシェーダーパスを適応する

import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js'

// TintShader: 画像に特定の効果(この場合はティント)を適用するためのシェーダー定義
const TintShader = {
    // uniforms: シェーダーに渡されるグローバルな変数
    uniforms:{
        tDiffuse: { value: null } // 元の画像テクスチャ用
        uTint: { value: null } //vec3型でRGB値を加算するため
    },
    // vertexShader: 頂点シェーダーのコード
    vertexShader: `
        varying vec2 vUv; // フラグメントシェーダーに渡すためのUV座標
        void main(){
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); // 頂点の位置を計算

            vUv = uv; // UV座標を設定
        }
    `,
    // fragmentShader: フラグメントシェーダーのコード
    fragmentShader: `
        uniform sampler2D tDiffuse; // 元の画像テクスチャ
        uniform vec3 uTint; //

        varying vec2 vUv; // 頂点シェーダーから受け取ったUV座標
        void main(){
            vec4 color = texture2D(tDiffuse, vUv); // UV座標に基づいてテクスチャから色を取得
            color.rgb += uTint; //元のテクスチャの色にuTintの値を加算
            gl_FragColor = color; // 取得した色をフラグメントの色として設定
        }
    `
}

// TintShaderをポストプロセッシングパスとして作成
const tintPass = new ShaderPass(TintShader)

//uTint.valueに新しいTHREE.Vector3オブジェクトを設定
tintPass.material.uniforms.uTint.value = new THREE.Vector3(-0.5,0.5,0)

// effectComposerにtintPassを追加
effectComposer.addPass(tintPass)

変位エフェクト
ジオメトリの頂点の位置を変更することで、物体の表面に目に見える変形を作り出す
*GammaCorrectionShaderの後、アンチエイリアシングの前に実装する

// 変位エフェクト用のシェーダー定義
const DisplacementShader = {
    uniforms: {
        tDiffuse: { value: null }, // 元の画像テクスチャ
        uTime: { value: null }     // アニメーション用の時間変数
    },
    vertexShader: `
        varying vec2 vUv; // UV座標をフラグメントシェーダーに渡すための変数

        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); // 頂点の位置を計算
            vUv = uv; // UV座標を設定
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse; // 元の画像テクスチャ
        uniform float uTime;        // アニメーション用の時間変数
        varying vec2 vUv;           // 頂点シェーダーから受け取ったUV座標

        void main() {
            // 新しいUV座標を計算(Y座標にサイン波を加えて変位させる)
            vec2 newUv = vec2(
                vUv.x,
                vUv.y + sin(vUv.x * 10.0 + uTime) * 0.05
            );
            vec4 color = texture2D(tDiffuse, newUv); // 変位したUV座標でテクスチャから色を取得

            gl_FragColor = color; // フラグメントの色として設定
        }
    `
}

// 変位エフェクト用のパスを作成し、エフェクトコンポーザーに追加
const displacementPass = new ShaderPass(DisplacementShader);
effectComposer.addPass(displacementPass);

// アニメーション時は関数内で
displacementPass.material.uniforms.uTime.value = elapsedTime; // 経過時間をパスの時間変数に設定

ノーマルマップテクスチャを使用して、UV座標に変位を加える

// 変位エフェクト用のシェーダー定義
const NormalMapDisplacementShader = {
    uniforms: {
        tDiffuse: { value: null },       // 元の画像テクスチャ
        uNormalMap: { value: null }      // 変位に使用するノーマルマップテクスチャ
    },
    vertexShader: `
        varying vec2 vUv; // UV座標をフラグメントシェーダーに渡すための変数

        void main() {
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); // 頂点の位置を計算
            vUv = uv; // UV座標を設定
        }
    `,
    fragmentShader: `
        uniform sampler2D tDiffuse; // 元の画像テクスチャ
        uniform sampler2D uNormalMap; // 変位に使用するノーマルマップテクスチャ

        varying vec2 vUv; // 頂点シェーダーから受け取ったUV座標

        void main() {
            vec3 normalColor = texture2D(uNormalMap, vUv).xyz * 2.0 - 1.0; // ノーマルマップから色情報を取得し変換
            vec2 newUv = vUv + normalColor.xy * 0.1; // 新しいUV座標を計算
            vec4 color = texture2D(tDiffuse, newUv); // 新しいUV座標でテクスチャから色を取得

            vec3 lightDirection = normalize(vec3(- 1.0, 1.0, 0.0)); // 光の方向
            float lightness = clamp(dot(normalColor, lightDirection), 0.0, 1.0); // 光の強さを計算
            color.rgb += lightness * 2.0; // 光の強さを色に加算

            gl_FragColor = color; // フラグメントの色として設定
        }
    `
}

// ノーマルマップ変位エフェクトのパスを作成し、エフェクトコンポーザーに追加
const normalMapdisplacementPass = new ShaderPass(NormalMapDisplacementShader);
normalMapdisplacementPass.material.uniforms.uNormalMap.value = textureLoader.load('ノーマルマップのパス');
effectComposer.addPass(normalMapdisplacementPass);

シェーダーを使ってパーティクルをアニメーションする

*パーティクルの各頂点を個別にアニメーション化するのではなく、GPUを使用して頂点シェーダー内で直接アニメーション化する

そもそもパーティクルのマテリアルではPointsMaterialを使いますが、これをShaderMaterialに置き換えます
PointsMaterialの実装は:https://koro-koro.com/threejs-no3/#chapter-8

sizeとsizeAttenuation

ShaderMaterialはsizesizeAttenuation(カメラからの距離に応じてどのように変化するかを制御)プロパティをサポートしていないので、シェーダー内で実装する必要があります
パーティクルはランダムなサイズを持ち、sizeAttenuationの機能を実装します

// パーティクルのジオメトリを作成
const particlesGeometry = new THREE.BufferGeometry();
const count = 2000; // パーティクルの数

const positions = new Float32Array(count * 3); // 位置データ vec3
const colors = new Float32Array(count * 3); // 色データ vec3
const scales = new Float32Array(count * 1); // スケール(サイズ) float

for (let i = 0; i < count * 3; i++) {
  positions[i] = (Math.random() - 0.5) * 2; // ランダムな位置
  colors[i] = Math.random(); // ランダムな色
}
for (let i = 0; i < count; i++) {
  scales[i] = Math.random(); // ランダムなスケール
}
/*
追記:3D空間内の点(x, y, z)や色(R, G, B)を扱うときに、可読性という観点から考えた場合
for(let i = 0; i < count; i++){
        const i3 = i * 3
        positions[i3    ] = Math.random()-0.5
        positions[i3 + 1] = Math.random()-0.5
        positions[i3 + 2] = Math.random()-0.5
        colors[i3    ] = Math.random()
        colors[i3 + 1] = Math.random()
        colors[i3 + 2] = Math.random()
        scales[i] = Math.random();
    }
*/

particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
particlesGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
// ジオメトリに属性に割り当てaScaleとしてカスタム属性をつける
particlesGeometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1));

// シェーダーマテリアルの作成
const material = new THREE.ShaderMaterial({
  uniforms: {
//パーティクルのサイズをuniformsに格納
    uSize: { value: 30 }, 
  },
  depthWrite: false, // 深度バッファへの書き込みを無効化(パーティクルが透明であるため)
  blending: THREE.AdditiveBlending, // 加算ブレンディングを使用(光のような効果)
  vertexColors: true, // 頂点色を有効にする
  vertexShader: ``,  // 頂点シェーダーのソースコード
  fragmentShader: ``  // フラグメントシェーダーのソースコード
});

// パーティクルシステムの作成とシーンへの追加
const particles = new THREE.Points(particlesGeometry, material);
scene.add(particles);
uniform float uSize;  

/*
備考:position、color、normalのような一般的な属性は、
ShaderMaterialを使用する際に頂点シェーダーに自動的に含まれます
aScaleのようなカスタム属性を定義してシェーダーに追加する場合は
頂点シェーダー内でこれらのカスタム属性を明示的に宣言する
*/
attribute float aScale;
//フラグメントシェーダーに色情報(vColor)を渡す
varying vec3 vColor;

void main(){
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);
  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;
  gl_Position = projectedPosition;
 //ランダムなサイズの実装
  gl_PointSize = uSize * aScale;
  /*
  sizeAttenuationの実装は
  パーティクルの基本サイズに
  1.0 / - viewPosition.zを掛ける
  */
  gl_PointSize *= 1.0 / - viewPosition.z;
  
  //vColorに色情報代入
  vColor = color;
}

パーティクルのサイズを高密度ディスプレイに対応させる

高密度ディスプレイほど、同じサイズのパーティクルが小さく表示される
そこで、uniformsに格納したuSizeのvalueにレンダラーのピクセル比を掛けます
*注意:レンダラーのインスタンス化の後にマテリアル定義すること

 const material = new THREE.ShaderMaterial({
     uniforms:{
        uSize: { value: 30 * renderer.getPixelRatio()},
    },  
   //...省略
   })

パーティクルのサイズを画面の高さに比例させる

Three.jsでのカメラのFOV(視野角)は、デフォルトでは垂直方向に固定されています
画面の高さが低くなればオブジェクトも小さくなります

パーティクルのサイズもオブジェクトと同じように画面の高さに比例するように実装
頂点シェーダーへの解像度(uResolution)を送り利用する
また高密度ディスプレイでも適切に保たれるようする

const material = new THREE.ShaderMaterial({
    vertexShader: vertexShader,
    fragmentShader: fragmentShader,
    uniforms:{
   
      //画面の幅と高さを渡す
       uResolution: {value: new THREE.Vector2(sizes.width, sizes.height) },
     }
 })

window.addEventListener('resize', () =>{
  
   //リサイズ時に更新
    material.uniforms.uResolution.value.set(sizes.width, sizes.height);
})
uniform float uSize;
uniform vec2 uResolution; 

void main(){
    vec4 modelPosition = modelMatrix * vec4(position, 1.0);
    vec4 viewPosition = viewMatrix * modelPosition;
    gl_Position = projectionMatrix * viewPosition;
  
    gl_PointSize = uSize * uResolution.y;  // 画面の高さをかける
    gl_PointSize *= 1.0 / - viewPosition.z;  //遠近法をシミュレート
}

*縦のサイズをかけてるだけなので、uSizeは小さい値にして調整する

パーティクルにパターンを適応

gl_PointCoord
gl_PointCoordはパーティクルのテクスチャマッピングに非常に便利な組み込み変数です
フラグメントシェーダー内でのみ利用可能で、レンダリングされる各ポイント(パーティクル)のフラグメントに対して、そのポイントの中での相対的な位置を(0,0)から(1,1)の範囲で表します
gl_PointCoord を使って円盤状、拡散点、光点などのパターンをパーティクルに描画することが可能

  void main(){
      gl_FragColor = vec4(gl_PointCoord, 1.0, 1.0); 
  }
void main(){
    //円盤状
   // float strength = distance(gl_PointCoord, vec2(0.5));
   // strength = step(0.5, strength);
   // strength = 1.0 - strength;
    
    // 拡散点
    float strength = distance(gl_PointCoord, vec2(0.5));
    strength *= 2.0;
    strength = 1.0 - strength;
    
    //光点
    // float strength = distance(gl_PointCoord, vec2(0.5));
    // strength = 1.0 - strength;
    // strength = pow(strength, 10.0);
    
    gl_FragColor = vec4(vec3(strength), 1.0);
  }

頂点シェーダーからフラグメントシェーダーに色情報(vColor)を受け取り
「mix関数」を使用して、黒色とvColorをstrength に基づいて混合したcolorを
gl_FragColorに割り当てる

//vColorを宣言  
varying vec3 vColor;

void main(){
  //...省略
  
  //黒色と頂点の色を混合して最終的な色を生成
    vec3 color = mix(vec3(0.0), vColor, strength);

    gl_FragColor = vec4(color, 1.0);  
  }

modelPositionを動かすアニメーション

頂点シェーダー内でアニメーション
uTimeというユニフォームをTHREE.ShaderMaterialのuniformsに追加
アニメーション関数内でその値を更新
頂点シェーダーで、modelPositionをuTimeを使って動かします

 const material = new THREE.ShaderMaterial({
     uniforms:{
        uSize: { value: 30 * renderer.getPixelRatio()},
        uTime: { value: 0 }
    },  
    //...省略
 })

//アニメーション関数
const clock = new THREE.Clock()
const tick = () =>{
    const elapsedTime = clock.getElapsedTime() 
  // uTimeを更新
    material.uniforms.uTime.value = elapsedTime
  //...省略
}
uniform float uSize;  
//経過時間
uniform float uTime;
 //...省略
void main(){
  vec4 modelPosition = modelMatrix * vec4(position, 1.0);

//アニメーションの実装部分はここから
   float posZ = fract(modelPosition.z + uTime * 0.3); // fract与えられた数値の小数部分
   modelPosition.z += posZ ;   // z軸の位置をずらす
//ここまで

  vec4 viewPosition = viewMatrix * modelPosition;
  vec4 projectedPosition = projectionMatrix * viewPosition;
  gl_Position = projectedPosition;

  //...省略
}

宇宙(実装例)

パーティクルは、カメラの視野の前後にランダムに散らばっている
パーティクルの位置を時間経過とともに微妙に変化させる
パーティクルのZ座標は、時間の経過によって連続的に常に0から1の範囲内で変動
実際には限られた範囲内で動いているだけですが、カメラに向かってくるような錯覚のアニメーション

パーティクルにテクスチャを使用

パーティクルにテクスチャを適用する際には、gl_PointCoordを使用します
*gl_PointCoordはフラグメントシェーダー内でのみ利用可能で
レンダリングされる各ポイント(パーティクル)のフラグメントに対して、そのポイントの中での相対的な位置を(0,0)から(1,1)の範囲で表します

uniform sampler2D uTexture;

void main() {
  //テクスチャから色情報を取得
    vec4 textureColor = texture2D(uTexture, gl_PointCoord);
    gl_FragColor = textureColor;
//グレースケールであれば、赤色(R)の値のみを取り出して使用し、それを透明度情報として活用することができます
    //float textureAlpha = texture2D(uTexture, gl_PointCoord).r;
    //gl_FragColor = vec4(1.0, 1.0, 1.0, textureAlpha);
}

備考:テクスチャを使用する際、テクスチャのY軸が反転する(つまり、上下逆になる)ことがあります
TextureオブジェクトのflipYプロパティは、テクスチャのY軸を反転させるかどうかを制御します
*デフォルトではtrue(自動的にY軸を反転させる)に設定されています

パーティクルの透明部分が背後の物体を隠さないように、深度バッファへの書き込みを無効化
THREE.AdditiveBlendingに設定すると、パーティクルが重なる部分がより明るく表示される

const material = new THREE.ShaderMaterial({
 transparent: true,
 depthWrite: false, // 深度バッファへの書き込みを無効化
 blending: THREE.AdditiveBlending // 加算ブレンディングを使用
});

パーティクルを球体上に分布させる

THREE.Sphericalは、3D空間内の点を球面座標系で表すためのクラスです
*特定の点がどこにあるかを計算するためのもので、直接ビジュアルには関係ない

球面座標系について
球面座標系では、点の位置は3つの値で表されます
半径(r)、垂直方向の角度(φ、phi)、そして水平方向の角度(θ、theta)

const spherical = new THREE.Spherical(radius, phi, theta);
/*
radiusは球の中心からの距離(半径)
phiは球の中心から点までの線が形成する垂直方向の角度です(0からπまで)
thetaは球の中心を通る水平面上での角度で、方位角を表します(0から2πまで)
*/

球面座標から3D位置(x, y, z座標)への変換
*THREE.Vector3オブジェクトを新しく作成し
setFromSpherical()メソッドを用いて
THREE.Sphericalオブジェクトから得た球面座標を直交座標系(x, y, z)に変換

const position = new THREE.Vector3();
position.setFromSpherical(spherical);
const positionsArray = new Float32Array(count * 3)
for(let i = 0; i < count; i++){
    const spherical = new THREE.Spherical(
       1,
       Math.random() * Math.PI,
       Math.random() * Math.PI * 2
     )
   const position = new THREE.Vector3()
   position.setFromSpherical(spherical)
      
   const i3 = i * 3
   positionsArray[i3    ] = position.x
   positionsArray[i3 + 1] = position.y
   positionsArray[i3 + 2] = position.z
}    

GSAPを用いたアニメーション

GSAPを使用して、パーティクルのアニメーションを制御し、アニメーションが完了した後にパーティクルをシーンから削除する
*アニメーションの進行度を表す、uProgressを作成して

import gsap from 'gsap'

const material = new THREE.ShaderMaterial({
   //省略
   uniforms:{
        //省略
     uProgress: new THREE.Uniform(0)
      },
})

const destroy = () => {
    scene.remove(firework) // シーンからパーティクルを削除
    geometry.dispose() // ジオメトリのリソースを解放
    material.dispose() // マテリアルのリソースを解放
}
/*
material.uniforms.uProgressの値が0から1まで3秒かけて変化
アニメーションが完了した時にdestroy関数を呼び出してクリーンアップ
*/
gsap.to(
  material.uniforms.uProgress,
  { value: 1, duration: 3, ease: 'linear', onComplete: destroy },
)

uProgressは0から1ですが、例えばこれを0から0.1で完了させたい場合
*アニメーションが最初の10%の間に進行する
ある範囲の値を別の範囲にマッピングするための関数があると便利です
GLSLには値をリマップする組み込み関数がないので作成します

uniform float uProgress;

//別の範囲にマッピングするための関数
float remap(float value, float originMin, float originMax, float destinationMin, float destinationMax){
    return destinationMin + (value - originMin) * (destinationMax - destinationMin) / (originMax - originMin);
}

//使用例
void main() {
   // positionは属性なので直接変更できません。そのためnewPositionを計算
    vec3 newPosition = position;
    
   // 0から0.1の範囲を0から1の新しい範囲に変換
    float explodingProgress = remap(uProgress, 0.0, 0.1, 0.0, 1.0);
   //uProgressが0.1より大きくなるっても、explodingProgressが1を超えないように制限
    explodingProgress = clamp(explodingProgress, 0.0, 1.0);
  //最初は遅く、終わりにかけて速く
   //explodingProgress = pow(explodingProgress, 3.0);
   //最初は速く、終わりにかけて遅く
    explodingProgress = 1.0 - pow(1.0 - explodingProgress, 3.0);
   // newPosition *= explodingProgress;  //mix関数を使用して滑らかに
  //explodingProgressが0の場合、結果はvec3(0.0)、explodingProgressが1の場合、結果はnewPosition
    newPosition = mix(vec3(0.0), newPosition, explodingProgress);
    vec4 modelPosition = modelMatrix * vec4(newPosition, 1.0);
   // その他のシェーダーコード...
}

mix関数は2つの値の間を線形に補間します
value1とvalue2は補間される2つの値またはベクトルです
factorは0から1の間の値で、value1からvalue2への補間をどれだけ進めるかを指定
factorが0の場合、結果はvalue1に等しく、factorが1の場合、結果はvalue2に等しい

mix(value1, value2, factor);

min関数を使用して、拡大フェーズと縮小フェーズを滑らかに連携させる
最大に達した後は終わりにかけて徐々に減速する

float openingProgress = remap(uProgress, 0.0, 0.125, 0.0, 1.0);
float closingProgress = remap(uProgress, 0.125, 1.0, 1.0, 0.0);
float progress = min(openingProgress, closingProgress);
progress = clamp(progress, 0.0, 1.0);

開始時のサイズ拡大(0.125に達した時点で1)と終了時のサイズ縮小(0.125から1.0の間で1から0に減少)を別々に計算し、そのうち小さい方を選択

  • 進行度が0.125未満の場合
    openingProgressは0から1へ増加しますが、closingProgressはこのフェーズではまだ1です
  • 進行度が0.125を超えた場合
    openingProgressは1に達して変化しなくなりますが、closingProgressは1から0へ減少し始めます

パーティクルが完全には消えないという問題に対処する
原因は、gl_PointSizeが1.0未満の値に対して最小値として1.0にクランプされるため、期待通りにパーティクルが消えないことです
*オペレーティングシステムやGPUによって異なる振る舞いする

 if(gl_PointSize < 1.0)
        gl_Position = vec4(9999.9);

花火(実装例)

既存のマテリアルを改変

Three.jsの組み込みマテリアル(この場合はMeshStandardMaterial)は、光源の取り扱い、環境マッピング、物理ベースのレンダリング、テクスチャタイプなど、複雑な機能をゼロから再構築するのは時間がかかるため、既存のマテリアルをベースにThree.jsのフックを使って、シェーダーがコンパイルされる前にカスタムコードを注入することができる

material.onBeforeCompile = (shader) =>{
    console.log(shader)
}

頂点シェーダー(vertexShader)、フラグメントシェーダー(fragmentShader)、ユニフォーム(uniforms)にアクセスし、これらを変更することが可能