three.jsで炎を作る

画像1枚からシェーダーとか使わないお手軽な感じで炎のアニメーションを作成してみました。（炎というより人魂っぽくなってる気もしますが・・）
![GIF 2020-11-20 16-00-59.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250171/99c44123-03ce-8543-d7f2-38efcdd4c5cd.gif)

#元になる画像を用意
imgフォルダを作成し、こんな感じのもやっぽい画像を「tex.png」として配置します。
お絵かきソフトで適当にブラーのフィルターとかを使って作りました。
（※ここでは見えづらいので背景を黒にしていますが、実際の画像は背景を透明色にします）
![tex.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/250171/bea5b080-9320-e107-7fd1-cd68802a308a.jpeg)

#threejsで炎パーティクルを作成
といってもパーティクルシステムとかは使わずに、単純にプレーン（板）を用意して、画像をテクスチャとして貼ったものを複数用意していきます。サイズ等はカメラ座標やウィンドウサイズに合わせて良きに設定します。

```javascript:three.js
  /* パーティクル設定用変数 */
  const fireParticles = []; //パーティクルを格納する配列
  const fireParticlesNum = 50; //パーティクルの数
  const firePos = -5; //パーティクルのy軸初期位置
  const fireSize = 20; //パーティクルサイズ

  /* テクスチャの用意 */
  const loader = new THREE.TextureLoader();
  const texture = loader.load('img/tex.png');

  /* 炎パーティクルを作成する */
  const geometry = new THREE.PlaneGeometry(fireSize, fireSize, 1);
  for(let i = 0; i < fireParticlesNum; i++){
    const material = new THREE.MeshLambertMaterial({
      map: texture, //読み込んだテクスチャを貼る
      transparent: true, //画像の透明度有効にする
    })
    /* パーティクルをランダムな座標に初期配置 */
    const particle = new THREE.Mesh(geometry, material);
    particle.position.y = Math.random() * fireSize + firePos;
    particle.position.z = Math.random() * 10;
    particle.rotation.z = Math.random() * 360;
    fireParticles.push(particle); //配列に格納
    scene.add(particle); //画面に追加
  }
```
#アニメーション作成する
レンダー関数の中でパーティクルの動きを設定します。
(1)でy軸（縦）方向の動きと画像の回転を設定しています。回転しつつ、ランダムな距離ずつ上に座標を移動させ、limitまで座標が移動したら初期位置に戻しています。
(2)扱いやすくするため、パーティクルのy座標を1(初期座標)～0(limit座標)に変換しています。
(3)パーティクルの大きさを上に行くほど小さく設定しています。
(4)sin関数でパーティクルのx座標を揺らすことで炎っぽくうねうねさせています。
(5)色を設定しています。
透明度：materialのopacityでyの値が0に近づくほど（上に行くほど）透明に
赤み：0.7ぐらいから上に行くほど強く
緑み：1から上に行くほど弱く(yの値そのまま)
青み：1から上に行くほど急激に弱く
となっていて、下のほうではちょっとだけ青みっぽく、真ん中は黄色っぽく、上のほうは赤っぽくなっています。

```javascript:three.js
  function render(){

    /* パーティクルのアニメーション設定 */
    for(let i = 0; i < fireParticlesNum; i++){

      /* (1)位置と回転 */
      const limit = fireSize*1.5; //炎が上昇する距離
      if(fireParticles[i].position.y < firePos+limit){
        fireParticles[i].position.y += Math.random()*(fireSize/20);
        fireParticles[i].rotation.z += 0.01;
      }else{
        fireParticles[i].position.y = firePos; //上昇限界位置まで行ったら初期位置に戻る
      }

      /* (2)y座標を1～0に変換 */
      let y = ((firePos+limit)-fireParticles[i].position.y)/limit;

      /* (3)大きさに変化をつける */
      fireParticles[i].scale.x = y*0.6; //上に行くほど横幅小さく
      fireParticles[i].scale.y = y; //上に行くほど横幅小さく

      /* (4)うねうね */
      let amp = (fireSize/15)*Math.random(); //うねうね大きさ
      let freq = 2*Math.random()+5; //うねうね量
      fireParticles[i].position.x = amp * Math.sin(freq*y*Math.PI);

      /* (5)色を付ける */
      fireParticles[i].material.opacity = Math.pow(y, 4); //上に行くほど透明に
      let r = Math.sin(Math.PI/4*y+Math.PI/2);
      let b = Math.pow(y, 20);
      fireParticles[i].material.color = new THREE.Color(r, y, b);
    }

    /* render回す */
    requestAnimationFrame(render);
    renderer.render(scene, camera);
  }
```

#根本を作る
発生源がゆらいでいると人魂感がさらに増してしまうので、根本の部分を作ります。
同じ画像テクスチャを使って、初期位置から動かさずにもやもやさせているだけです。もやもやを根本に重ねることで発光している感じも出ます。
まずはさっきとほとんど同じ要領で根本用のメッシュを作成して画面に追加します。
最初に青みっぽい色を設定しているのと初期位置をじゃかん下げているぐらいです。

```javascript:three.js
  const fireRootNum = 2;

  /* 根本の炎作る */
  const fireRoot = []; //パーティクルを格納する配列
  for(let i = 0; i < fireRootNum; i++){
    const material = new THREE.MeshLambertMaterial({
      map: texture,
      transparent: true,
      color: new THREE.Color(0.85,0.85,1.0) //青みつける
    })
    const particle = new THREE.Mesh(geometry,material);
    particle.position.y = firePos-2;
    fireRoot.push(particle);
    scene.add(particle);
  }
```
render関数内でアニメーションを付けます。
ランダムで透明度を変えたりサイズを変えたり回転させてりしてもやもやさせています。

```javascript:three.js
    /* 根本のアニメーション */
    for(let i = 0; i < fireRootNum; i++){
      fireRoot[i].material.opacity = Math.random();
      let size = 0.5*Math.random() + 0.5;
      fireRoot[i].scale.y = size;
      fireRoot[i].rotation.z = Math.random()*Math.PI*2;
    }
```

コード全体: https://github.com/rucochanman/web/blob/main/threejs/fire/three.js