バレンタインなんであめを降らせた

## はじめに
本日2/14はバレンタインデー。
「もしかすると机の中や靴箱にチョコが入っているかも」とかいうドキドキした気持ちなんて十数年前から忘れてしまった自分ですが、こんな日くらいは甘い気持ちに包まれたいと思い、あめを降らせてみることにしました。


## 完成品
https://nbayashi.github.io/cesium_raincandy/index.html

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/70ea5e36-c65c-e7d8-a07a-9dbad0265d11.gif" width=150%>


その名の通り「あめちゃん」を降らせています。


あめのほかにもチョコや
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/386f7046-6aba-f21f-6f9e-011c6e00ecec.png)

リンゴ飴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/599769d8-4619-7b59-162a-85e7793fac62.png)

任意の画像をアップロードして降らせることもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/c19804ec-1661-7802-acd9-2547b4e3ee5f.png)


## 少し解説
完全に出オチネタなので、少しコードの説明。
オープンソースのGISエンジン、Cesiumを使っています。
Cesiumの基本的なはじめかたはこちらを参照。

https://qiita.com/rot/items/bf7434d299e58d4c5a2a



CesiumのSandcastleから、[Particle System Weather](https://sandcastle.cesium.com/?src=Particle%20System%20Weather.html&label=All)を参考にしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/af74b9aa-4184-e067-e363-a699e8056ec0.png)

これは、Cesium上でサポートしているパーティクルで表現しています。
パーティクルについての詳しい説明は[公式サイト](https://cesium.com/learn/cesiumjs-learn/cesiumjs-particle-systems/)をご覧いただければ幸いですが、こんな表現や、
![](https://images.prismic.io/cesium/tutorials-particle-systems-sphere-emitter.gif)
こんな表現
![](https://images.prismic.io/cesium/tutorials-particle-systems-particles1.gif?)
ができるようです。


あめの表現としては以下のようにパーティクルを発生しています。
```jsx

const viewer = new Cesium.Viewer("cesiumContainer", {
          shouldAnimate: true,
          terrain: Cesium.Terrain.fromWorldTerrain(),
        });
        
const scene = viewer.scene;

const snowParticleSize = 12.0;
const snowRadius = 100000.0;
const minimumSnowImageSize = new Cesium.Cartesian2(
    snowParticleSize,
    snowParticleSize
    );

const maximumSnowImageSize = new Cesium.Cartesian2(
    snowParticleSize * 2.0,
    snowParticleSize * 2.0
    );

let snowGravityScratch = new Cesium.Cartesian3();

//callback
const snowUpdate = function (particle, dt) {
    snowGravityScratch = Cesium.Cartesian3.normalize(
        particle.position,
        snowGravityScratch
        );
    Cesium.Cartesian3.multiplyByScalar( //重力の強さ
        snowGravityScratch,
        Cesium.Math.randomBetween(-10.0, -50.0),
        snowGravityScratch
        );
    particle.velocity = Cesium.Cartesian3.add(
        particle.velocity,
        snowGravityScratch,
        particle.velocity
        );    
    
    };


function startCandy(image_file) {

        //カラーをランダムに決定
          let rand_red= Math.floor( Math.random()  *100)/100;
          let rand_green = Math.floor( Math.random()  *100)/100;
          let rand_blue= Math.floor( Math.random()  *100)/100;
          let col = new Cesium.Color(rand_red, rand_green, rand_blue, 1);


          scene.primitives.removeAll();
          scene.primitives.add(
            new Cesium.ParticleSystem({
              modelMatrix: new Cesium.Matrix4.fromTranslation(
                scene.camera.position
              ),
              minimumSpeed: 1.0,  //particleのランダムスピードの最小値
              maximumSpeed: 2.0,  //particleのランダムスピードの最大値
              lifetime: 15.0,
              emitter: new Cesium.SphereEmitter(1000),  //particleの種類
              startScale: 2,  //particleのスタート時のサイズ
              endScale: 1.0,  //particleの終了時のサイズ
              image: image_file,  //画像ファイル
              emissionRate: 700.0, //particleの量/秒
              startColor: col,  //particleのスタート時の色（今回はランダムの色）
              endColor: Cesium.Color.WHITE.withAlpha(1.0),  //particleの終了時の色
              minimumImageSize: minimumSnowImageSize,
              maximumImageSize: maximumSnowImageSize,
              updateCallback: rainUpdate,
            })
          );
        }

```


`scene.primitives.add()`でパーティクルをシーンに追加して、`new Cesium.ParticleSystem()`で発生させるパーティクルのプロパティを設定するようです。

html部分はメニューを追加しただけで、特に何も説明するものはありません。
```html

<div id="cesiumContainer" class="cesium"></div>

<div id = "menubar">
  <div>
    <div>降飴量</div>
    <input id ="range_slide" type="range" min="0.0" max="1000.0" step="1" data-bind="value: height, valueUpdate: 'input'">
  </div>
  <div>
    <select name="menu" id="menu">
      <option value="img/candy.png">あめ</option>
      <option value="img/heart_love.png">チョコ</option>
      <option value="img/ringoame.png">りんご飴</option>
      <option value="other">アップロード</option>
    </select>
    <input type="file" id="upload_img" name="upload_img" accept="image/png, image/jpeg" />
  </div>
  <div>
    <button id="button_reset">リセット</button>
  </div>
</div>

```

「あめ」を降らすのに手を加えたParticleSystemの主なプロパティを紹介します。

### emitter
発生させるパーティクルの形状を指定します。
```jsx
emitter: new Cesium.BoxEmitter(new Cesium.Cartesian3(0.5, 0.5, 0.5))
```

とすればボックス状にパーティクルを発生

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/1c11ba35-cb46-415f-2afc-3b9546795196.png" width=200>


```jsx
emitter: new Cesium.CircleEmitter(0.5)
```

とすれば円形にパーティクルを発生

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/56d0895f-352b-0b79-068c-2f76f2fdeefd.png" width=200>


```jsx
emitter: new Cesium.ConeEmitter(Cesium.Math.toRadians(30.0))
```

円錐形にパーティクルを発生

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/ea91add0-dbda-243d-3904-786f139ef8f3.png" width=200>


```jsx
emitter: new Cesium.SphereEmitter(Cesium.Math.toRadians(30.0))
```

とすれば球形にパーティクルを発生させることができます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/131ba4e4-ca71-4278-7cd2-aa748be77832.png" width=200>


同じくSandCastleに上がっている[Fireworks](https://sandcastle.cesium.com/?src=Particle%20System%20Fireworks.html&label=All)もSphereでパーティクルを発生させているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/a0541c60-7757-3830-09af-d1a75577442d.png)

### image
パーティクルで表示させる画像のURLを指定します。
今回は、「あめ」のほか、チョコレートやリンゴ飴、あとアップロードした画像も降らせることができるようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/2ec5a6e7-4c01-ce8c-baf2-92b253ad27cd.png)

それぞれの選択に合わせて、このイメージURLを変更してパーティクル発生を実行しています。
```jsx
document.getElementById("menu").addEventListener("change", function () {
          if(document.getElementById("menu").value=="other"){
            document.getElementById("upload_img").style.display = "block";
          }else{
            document.getElementById("upload_img").style.display = "none";
            let value = document.getElementById("menu").value;
            startCandy(value);
          }
        });
```

### emissionRate

１秒間におけるパーティクルを発生させる量を指定します。
降雨量ならぬ降飴量として、スライドバーを設けてこのemissionRateの数値を変化できるようにしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/c96df151-a548-0e64-c226-fb0e1b811595.png)

```jsx
document.getElementById("range_slide").addEventListener("input", function () {
  let rangeValue = document.getElementById("range_slide").value;
  scene.primitives._primitives[0].emissionRate = rangeValue;
});
```
emissionRateが小さい時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/8b40b5ed-1203-8cba-5cd0-7e2664b59ccf.png)

emissionRateが大きい時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/b59835a7-0662-b6e5-e94b-c237be33b37c.png)



### updateCallback

パーティクルの更新として、毎フレーム呼び出されるコールバック関数を指定しています。コールバック関数の内容は以下のようになっていますが、ここでそれぞれのパーティクルの重力やスピードなどをランダムに変化させているようです。
```jsx
//callback
const snowUpdate = function (particle, dt) {
    snowGravityScratch = Cesium.Cartesian3.normalize(
        particle.position,
        snowGravityScratch
    );
    Cesium.Cartesian3.multiplyByScalar( //重力の強さ
        snowGravityScratch,
        Cesium.Math.randomBetween(-10.0, -50.0),
        snowGravityScratch
    );
    particle.velocity = Cesium.Cartesian3.add(
        particle.velocity,
        snowGravityScratch,
        particle.velocity
    );    
    
};
```

そのほかのプロパティは[こちら](https://cesium.com/learn/cesiumjs/ref-doc/ParticleSystem.html)のドキュメントをご覧ください。



## リセットボタン
今回のパーティクルはカメラの表示位置に対してパーティクルが発生しているので、地図を移動すると降水域（降飴域）の外に出る。その際はリセットを押してパーティクルを発生し直せるようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/0a12a7e3-5712-23bd-1c82-7290e47bc280.png)

```jsx
document.getElementById("button_reset").addEventListener("click", function () {
  let value = scene.primitives._primitives[0].image;
  startCandy(value);
});
```

## 思ってたんとちゃう
このコールバック関数内に、パーティクルの色をランダムで設定されるようにすれば、いろんな色のあめが降ってきて、さらに幸せな気持ちになるんちゃう、と思い以下を追記したところ


```jsx
particle.startColor.red= Math.floor( Math.random()  *100)/100;
particle.startColor.green = Math.floor( Math.random()  *100)/100;
particle.startColor.blue= Math.floor( Math.random()  *100)/100;
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/89227d00-ec52-856b-6bf6-14e0051df48a.png" width=200>


ゲーミングキャンディーが降ってくるようになった・・・。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61492/987de505-2686-00b2-a1e2-46c73960c89f.gif" width=150%>


思ってたんとちゃうけど、これはこれで楽しいからいっか。


パーティクル、ちょっと難しいイメージやったけど、ちょっと仲良くできてよかった。
皆さんも少しでも甘い気持ちになってくれれば幸いです。