ブラウザのウィンドウをスクリーン上で動かした時の位置の変化が Babylon.js で描画したパーティクルの数・背景の色などに影響する処理【Babylon.js-3】

この記事は「[Babylon.js Advent Calendar 2023](https://qiita.com/advent-calendar/2023/babylonjs)」の 20日目の記事です。

## 今回の内容
この記事では、以下の記事で書いていた「スクリーン上でブラウザのウィンドウを動かしたら、そのウィンドウの移動（位置）が描画に影響する」という内容と、Babylon.js のパーティクルの描画を組み合わせてみようと思います。

●「スクリーン上でブラウザのウィンドウを動かしたら描画に影響」「2つのブラウザのウィンドウの一方を動かすと他方に影響」という方向の実装作品の技術メモ - Qiita
　https://qiita.com/youtoy/items/945edb13856417c8c901

### 実行した結果
まず、今回の内容を実行した結果から紹介します。
以下の動画内の内容についてざっくり書くと、ウィンドウが画面の上のほうにあるか、少し真ん中よりの位置（もしくはそれより下）にあるかで、パーティクルの数・背景の色が変わります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/8vQz9AR3BX0?si=IyAJ3LAiCWrSEy9u" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

## 作成した内容
それでは、実際に作った内容についてなど、以下で説明していきます。

### 元にした内容
今回のパーティクルを使った描画の部分は、公式のドキュメントに掲載されているサンプルを使って試しました。サンプルの掲載元と内容は以下のとおりです。

#### 元にしたサンプルの掲載元： 公式ドキュメント
●Particle System | Babylon.js Documentation
　https://doc.babylonjs.com/features/featuresDeepDive/particles/particle_system

#### 元にしたサンプルの内容： Babylon.js Playground上で公開されているもの
●Particle System Examples | Babylon.js Playground
　https://playground.babylonjs.com/#0K3AQ2#3067

```javascript
var createScene = function () {
    var scene = new BABYLON.Scene(engine);

    // Setup environment
    var light0 = new BABYLON.PointLight("Omni", new BABYLON.Vector3(0, 2, 8), scene);
    var camera = new BABYLON.ArcRotateCamera("ArcRotateCamera", -Math.PI /2, 7 * Math.PI / 16, 20, new BABYLON.Vector3(0, 0, 0), scene);
    camera.attachControl(canvas, true);

    // Ground
    var ground = BABYLON.Mesh.CreatePlane("ground", 50.0, scene);
    ground.position = new BABYLON.Vector3(0, -50, 0);
    ground.rotation = new BABYLON.Vector3(Math.PI / 2, 0, 0);

    ground.material = new BABYLON.StandardMaterial("groundMat", scene);
    ground.material.backFaceCulling = false;
    ground.material.diffuseColor = new BABYLON.Color3(0.3, 0.3, 1);

    // Create a particle system
    var particleSystem = new BABYLON.ParticleSystem("particles", 2000, scene);

    //Texture of each particle
    particleSystem.particleTexture = new BABYLON.Texture("textures/flare.png", scene);

    // Where the particles come from
    particleSystem.emitter = BABYLON.Vector3.Zero(); // the starting position
    particleSystem.minEmitBox = new BABYLON.Vector3(-1, -1, -1); // Bottom Left Front
    particleSystem.maxEmitBox = new BABYLON.Vector3(1, 1, 1); // Top Right Back

    // Colors of all particles
    particleSystem.color1 = new BABYLON.Color4(0.7, 0.8, 1.0, 1.0);
    particleSystem.color2 = new BABYLON.Color4(0.2, 0.5, 1.0, 1.0);
    particleSystem.colorDead = new BABYLON.Color4(0, 0, 0.2, 0.0);

    // Size of each particle (random between...
    particleSystem.minSize = 0.1;
    particleSystem.maxSize = 0.5;

    // Life time of each particle (random between...
    particleSystem.minLifeTime = 0.3;
    particleSystem.maxLifeTime = 1.5;

    // Emission rate
    particleSystem.emitRate = 1500;

    // Blend mode : BLENDMODE_ONEONE, or BLENDMODE_STANDARD
    particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ONEONE;

    // Set the gravity of all particles
    particleSystem.gravity = new BABYLON.Vector3(0, -9.81, 0);

    // Direction of each particle after it has been emitted
    particleSystem.direction1 = new BABYLON.Vector3(-7, 8, 3);
    particleSystem.direction2 = new BABYLON.Vector3(7, 8, -3);

    // Angular speed, in radians
    particleSystem.minAngularSpeed = 0;
    particleSystem.maxAngularSpeed = Math.PI;

    // Speed
    particleSystem.minEmitPower = 1;
    particleSystem.maxEmitPower = 3;
    particleSystem.updateSpeed = 0.005;

    // Start the particle system
    particleSystem.start();

        
    var LTMin = function(value) {
        particleSystem.minLifeTime = value;
    }

    var LTMax = function(value) {
        particleSystem.maxLifeTime = value;
    }

    var updateLabelMinLT = function(value) {
        return value.toFixed(3);  
    }

    var updateLabelMaxLT = function(value) {
        return value.toFixed(3);  
    }

    var emitRate = function(value) {
        particleSystem.emitRate = value;
    }

    var updateLabelRate = function(value) {
        return value.toFixed(0);
    }

    var emitMinPow = function(value) {
        particleSystem.minEmitPower = value;
    }

    var emitMaxPow = function(value) {
        particleSystem.maxEmitPower = value;
    }

    var updateLabelMinPow = function(value) {
        return value.toFixed(3); 
    }

    var updateLabelMaxPow = function(value) {
        return value.toFixed(3); 
    }

    var updateSpeed = function(value) {
        particleSystem.updateSpeed = value;
    }

    var updateLabelSpeed = function(value) {
        return value.toFixed(5);  
    }

    var advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");

    var selectBox = new BABYLON.GUI.SelectionPanel("sp");
    selectBox.width = 0.25;
    selectBox.height = 0.9;
    selectBox.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
    selectBox.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
     
    advancedTexture.addControl(selectBox);
	
	var sliderLifeTime = new BABYLON.GUI.SliderGroup("LifeTime", "S");
	sliderLifeTime.addSlider("Min ", LTMin, "seconds", 0, 20, 0.3, updateLabelMinLT); 
    sliderLifeTime.addSlider("Max ", LTMax, "seconds", 0, 20, 1.5, updateLabelMaxLT);

    selectBox.addGroup(sliderLifeTime);

    var sliderEmitRate = new BABYLON.GUI.SliderGroup("Emit Rate", "S");
	sliderEmitRate.addSlider("Rate  ", emitRate, "units", 0, 2000, 1500, updateLabelRate); 

    selectBox.addGroup(sliderEmitRate);

    var sliderPower = new BABYLON.GUI.SliderGroup("Power", "S");
	sliderPower.addSlider("Min ", emitMinPow, "units", 0, 5, 1, updateLabelMinPow); 
    sliderPower.addSlider("Max ", emitMaxPow, "units", 0, 5, 3, updateLabelMaxPow);

    selectBox.addGroup(sliderPower);

    var sliderUpdateSpeed = new BABYLON.GUI.SliderGroup("Update Speed", "S");
	sliderUpdateSpeed.addSlider("Speed ", updateSpeed, "units", 0, 0.1, 0.01, updateLabelSpeed); 

    selectBox.addGroup(sliderUpdateSpeed);


    return scene;
}
```

これは、以下のような見た目になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9d3f25ae-ee1e-85a1-db2c-831843ff9c80.png)

### 変更を加えた後の内容
上で掲載した元のプログラムに対して、スクリーン上でのブラウザのウィンドウの位置に応じた処理を付け加えます。

まずは、他の部分を含めた全体を紹介します。
元の内容が  Babylon.js Playground上で提供されていたので、それを直接書きかえて（主には処理の追記を行って）動かしました。

```javascript
let scene, particleSystem;
let eRate, col1, col2;

function checkWindowMove() {
  const screenTop = window.screenTop;

  if (screenTop < 200) {
    scene.clearColor = new BABYLON.Color4(0.2, 0.3, 0.3, 1);
    eRate = 300;
    col1 = new BABYLON.Color4(0.9, 0.7, 0.5, 1.0);
    col2 = new BABYLON.Color4(0.9, 0.5, 0.2, 1.0);
  } else {
    scene.clearColor = new BABYLON.Color4(0, 0, 0, 1);
    eRate = 1500;
    col1 = new BABYLON.Color4(0.7, 0.8, 1.0, 1.0);
    col2 = new BABYLON.Color4(0.2, 0.5, 1.0, 1.0);
  }
  particleSystem.emitRate = eRate;

  particleSystem.color1 = col1;
  particleSystem.color2 = col2;
}
setInterval(checkWindowMove, 500);

const createScene = function () {
  scene = new BABYLON.Scene(engine);
  scene.clearColor = new BABYLON.Color4(0, 0, 0, 1);

  // Setup environment
  const light0 = new BABYLON.PointLight(
    "Omni",
    new BABYLON.Vector3(0, 2, 8),
    scene
  );
  const camera = new BABYLON.ArcRotateCamera(
    "ArcRotateCamera",
    -Math.PI / 2,
    (7 * Math.PI) / 16,
    20,
    new BABYLON.Vector3(0, 0, 0),
    scene
  );
  camera.attachControl(canvas, true);

  // Ground
  const ground = BABYLON.Mesh.CreatePlane("ground", 50.0, scene);
  ground.position = new BABYLON.Vector3(0, -50, 0);
  ground.rotation = new BABYLON.Vector3(Math.PI / 2, 0, 0);

  ground.material = new BABYLON.StandardMaterial("groundMat", scene);
  ground.material.backFaceCulling = false;
  ground.material.diffuseColor = new BABYLON.Color3(0.3, 0.3, 1);

  // Create a particle system
  particleSystem = new BABYLON.ParticleSystem("particles", 2000, scene);

  //Texture of each particle
  particleSystem.particleTexture = new BABYLON.Texture(
    "textures/flare.png",
    scene
  );

  // Where the particles come from
  particleSystem.emitter = BABYLON.Vector3.Zero(); // the starting position
  particleSystem.minEmitBox = new BABYLON.Vector3(-1, -1, -1); // Bottom Left Front
  particleSystem.maxEmitBox = new BABYLON.Vector3(1, 1, 1); // Top Right Back

  // Colors of all particles
  particleSystem.color1 = new BABYLON.Color4(0.7, 0.8, 1.0, 1.0);
  particleSystem.color2 = new BABYLON.Color4(0.2, 0.5, 1.0, 1.0);
  particleSystem.colorDead = new BABYLON.Color4(0, 0, 0.2, 0.0);

  // Size of each particle (random between...
  particleSystem.minSize = 0.1;
  particleSystem.maxSize = 0.5;

  // Life time of each particle (random between...
  particleSystem.minLifeTime = 0.3;
  particleSystem.maxLifeTime = 1.5;

  // Emission rate
  eRate = 1500;
  particleSystem.emitRate = eRate;

  // Blend mode : BLENDMODE_ONEONE, or BLENDMODE_STANDARD
  particleSystem.blendMode = BABYLON.ParticleSystem.BLENDMODE_ONEONE;

  // Set the gravity of all particles
  particleSystem.gravity = new BABYLON.Vector3(0, -9.81, 0);

  // Direction of each particle after it has been emitted
  particleSystem.direction1 = new BABYLON.Vector3(-7, 8, 3);
  particleSystem.direction2 = new BABYLON.Vector3(7, 8, -3);

  // Angular speed, in radians
  particleSystem.minAngularSpeed = 0;
  particleSystem.maxAngularSpeed = Math.PI;

  // Speed
  particleSystem.minEmitPower = 1;
  particleSystem.maxEmitPower = 3;
  particleSystem.updateSpeed = 0.005;

  // Start the particle system
  particleSystem.start();

  const LTMin = function (value) {
    particleSystem.minLifeTime = value;
  };

  const LTMax = function (value) {
    particleSystem.maxLifeTime = value;
  };

  const updateLabelMinLT = function (value) {
    return value.toFixed(3);
  };

  const updateLabelMaxLT = function (value) {
    return value.toFixed(3);
  };

  const emitRate = function (value) {
    particleSystem.emitRate = value;
  };

  const updateLabelRate = function (value) {
    return value.toFixed(0);
  };

  const emitMinPow = function (value) {
    particleSystem.minEmitPower = value;
  };

  const emitMaxPow = function (value) {
    particleSystem.maxEmitPower = value;
  };

  const updateLabelMinPow = function (value) {
    return value.toFixed(3);
  };

  const updateLabelMaxPow = function (value) {
    return value.toFixed(3);
  };

  const updateSpeed = function (value) {
    particleSystem.updateSpeed = value;
  };

  const updateLabelSpeed = function (value) {
    return value.toFixed(5);
  };

  const advancedTexture =
    BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");

  const selectBox = new BABYLON.GUI.SelectionPanel("sp");
  selectBox.width = 0.25;
  selectBox.height = 0.9;
  selectBox.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
  selectBox.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;

  advancedTexture.addControl(selectBox);

  const sliderLifeTime = new BABYLON.GUI.SliderGroup("LifeTime", "S");
  sliderLifeTime.addSlider(
    "Min ",
    LTMin,
    "seconds",
    0,
    20,
    0.3,
    updateLabelMinLT
  );
  sliderLifeTime.addSlider(
    "Max ",
    LTMax,
    "seconds",
    0,
    20,
    1.5,
    updateLabelMaxLT
  );

  selectBox.addGroup(sliderLifeTime);

  const sliderEmitRate = new BABYLON.GUI.SliderGroup("Emit Rate", "S");
  sliderEmitRate.addSlider(
    "Rate  ",
    emitRate,
    "units",
    0,
    2000,
    1500,
    updateLabelRate
  );

  selectBox.addGroup(sliderEmitRate);

  const sliderPower = new BABYLON.GUI.SliderGroup("Power", "S");
  sliderPower.addSlider(
    "Min ",
    emitMinPow,
    "units",
    0,
    5,
    1,
    updateLabelMinPow
  );
  sliderPower.addSlider(
    "Max ",
    emitMaxPow,
    "units",
    0,
    5,
    3,
    updateLabelMaxPow
  );

  selectBox.addGroup(sliderPower);

  const sliderUpdateSpeed = new BABYLON.GUI.SliderGroup("Update Speed", "S");
  sliderUpdateSpeed.addSlider(
    "Speed ",
    updateSpeed,
    "units",
    0,
    0.1,
    0.01,
    updateLabelSpeed
  );

  selectBox.addGroup(sliderUpdateSpeed);

  return scene;
};
```

#### 付け加えた主な部分
主に変更したというか付け加えた部分は、冒頭の部分です。

```javascript
let scene, particleSystem;
let eRate, col1, col2;

function checkWindowMove() {
  const screenTop = window.screenTop;

  if (screenTop < 200) {
    scene.clearColor = new BABYLON.Color4(0.2, 0.3, 0.3, 1);
    eRate = 300;
    col1 = new BABYLON.Color4(0.9, 0.7, 0.5, 1.0);
    col2 = new BABYLON.Color4(0.9, 0.5, 0.2, 1.0);
  } else {
    scene.clearColor = new BABYLON.Color4(0, 0, 0, 1);
    eRate = 1500;
    col1 = new BABYLON.Color4(0.7, 0.8, 1.0, 1.0);
    col2 = new BABYLON.Color4(0.2, 0.5, 1.0, 1.0);
  }
  particleSystem.emitRate = eRate;

  particleSystem.color1 = col1;
  particleSystem.color2 = col2;
}
setInterval(checkWindowMove, 500);
```

あまり凝ったことはしておらず、一定の時間間隔で `window.screenTop` を取得し、その値を使った処理をしています。
そして、その取得した値が適当な閾値より小さいかどうかによって、背景の色・パーティクルの色・発生量に関わるパラメータを変更している形です。

## おわりに
今回、ブラウザのウィンドウをスクリーン上で動かした時に、その位置の変化が Babylon.js で描画したパーティクルのパラメータに影響を及ぼすという内容を試しました。

今回の内容は、とりあえずの動作確認という感じだったので、さらに色々試せればと思います。

### 今回対応できてないこと
今回、これもできたらと思って、対応しきれなかったことのメモです。

ウィンドウを揺らしたら、その揺れが画面内に伝わるようなイメージのこともできればと思ったのですが、時間が足りずでした。ちなみに、そのような挙動をする描画について、p5.js を使ったものは、前に以下を作っていたりしました。

https://twitter.com/youtoy/status/1728214364111691904