【Three.js】マウスに追従するライト

2021年9月25日JavaScript,Three.js

なかなか良い雰囲気

はじめに

マウスの周りだけボンヤリ明るい。みたいなモノを作ってみましょう。

できた

デモサイトはこちら

簡単にできるかなぁと思ったんですけど、意外と手こずりましたね。そこを重点的に解説していこうと思います。

座標を変換しないといけない

ディスプレイ上のマウスの座標はコレで取得できます↓。

document.body.addEventListener("mousemove", function(e) {
    var dispX = e.clientX;
    var dispY = e.clientY;
});

これをライトの"position.x"と"position.y"に入れてあげればいい。とか思っちゃいそうだけどそんな甘くない。なぜならライトは3D空間上のモノだから。

だからこのディスプレイ上の座標を3D空間の座標に変換してあげる必要があります。

まずは座標を-1~1の間の値に変換する

まず気を付けないといけない事は、ディスプレイと3D空間とでは原点の位置が違うということ。ディスプレイは左上が原点で、3Dは真ん中。そこも意識しつつ、ディスプレイ上の座標を-1~1の間に変換します。それがコレ↓。

// ディスプレイ上でのマウスの座標(-1~1に変換)
var dispX = (e.clientX / window.innerWidth) * 2 - 1;
var dispY = -(e.clientY / window.innerHeight) * 2 + 1;

言葉で説明するのがムズカシイので絵にしました。X座標が40の時の例です。

とてもよく分かりましたね。で、これで求めたdispXに3D空間の幅を、dispYに3D空間の高さをかけてあげれば、3D空間上の座標に変換できるというワケですね。大丈夫。よく考えれば分かるハズです。

3D空間の幅と高さを求める

3D空間の幅と高さは、カメラからの距離とカメラの視野角から求めることができます↓。

// 3D空間の高さと幅
var heightOnOrigin = (Math.tan(((fov * Math.PI / 180) / 2)) * (camera.position.z - plZ) * 2)
var widthOnOrigin = heightOnOrigin * aspRatio

計算式がゴチャゴチャして分かりづらいですね。絵にしました。ライトが動く面の高さを求めています。

幅は「高さ×カメラのアスペクト比」です。

この高さと幅の2分の1を、さっき求めた-1~1の値にかけてあげれば、3D空間上の座標がでます。

// ライトの座標
pointLight.position.set(dispX * widthOnOrigin / 2, dispY * heightOnOrigin / 2, plZ);

なぜ2分の1なのかと言うと、3D空間の原点は真ん中だからです。これもがんばれば分かります。

全コードを載せておきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>妖怪FILE</title>
  <script type="text/javascript" src="../libs/three.js"></script>
  <style>
      html, body {
        margin: 0;
        overflow: hidden;
        height: 100%;
      }
      #screen {
        height: 100%;
      }
  </style>
</head>

<body>

  <div id="screen"></div>
  
  <script type="text/javascript">

    window.onload = function() {

      // シーン
      var scene = new THREE.Scene();
  
      /***** カメラの設定 *****/
      // アスペクト比
      var aspRatio = window.innerWidth / window.innerHeight;

      // 視野角
      var fov;
      if (aspRatio > 1) {
        fov = 35;
      } else if (aspRatio > 0.9) {
        fov = 45
      } else if (aspRatio > 0.8) {
        fov = 50;
      } else if (aspRatio > 0.6) {
        fov = 60;
      } else if (aspRatio > 0.5) {
        fov = 70;
      } else {
        fov = 80;
      }
      var camera = new THREE.PerspectiveCamera(fov, aspRatio, 0.1, 1000);
      camera.position.z = 20;
      camera.lookAt(scene.position);
  
      // レンダラー
      var renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.getElementById("screen").appendChild(renderer.domElement);
      
      // ライト   
      var pointLight = new THREE.PointLight(0xffffff);
      pointLight.position.set(100, 100, 100);
      pointLight.decay = 1;
      pointLight.intensity = 4;
      pointLight.distance = 20;
      pointLight.visible = false;
      scene.add(pointLight);

      // テクスチャ
      var textureLoader = new THREE.TextureLoader();
      var texture = textureLoader.load("../assets/textures/yokai12.jpg");
      
      // パネル
      var panelGeometry = new THREE.PlaneGeometry(14, 10.5, 1, 1);
      var panelMaterial = new THREE.MeshPhongMaterial();
      panelMaterial.map = texture;
      var panel = new THREE.Mesh(panelGeometry, panelMaterial);
      scene.add(panel);  

      /***** 描画関数 *****/
      function renderScene() {
        renderer.render(scene, camera);
        requestAnimationFrame(renderScene);
      }
      
      /***** スマホ用ライトの位置設定関数 *****/
      function setLightPositionForSP(e) {
        // ライトのZ座標
        const plZ = 0.1;
        
        // ディスプレイ上でのタッチの座標(-1~1に変換)
        var dispX = (e.changedTouches[0].clientX / window.innerWidth) * 2 - 1;
        var dispY = -(e.changedTouches[0].clientY / window.innerHeight) * 2 + 1;
        
        // 3D空間の高さと幅
        var heightOnOrigin = (Math.tan(((fov * Math.PI / 180) / 2)) * (camera.position.z - plZ) * 2)
        var widthOnOrigin = heightOnOrigin * aspRatio
        
        // ライトの座標を設定
        pointLight.position.set(dispX * widthOnOrigin / 2, dispY * heightOnOrigin / 2, plZ);
      }

      /***************************************/
      /***** ライトの座標を設定 ここから *****/
      /***************************************/
      /***** PC用 *****/
      document.body.addEventListener("mousemove", function(e) {

        pointLight.visible = true;

        // ライトのZ座標
        const plZ = 0.1;

        // ディスプレイ上でのマウスの座標(-1~1に変換)
        var dispX = (e.clientX / window.innerWidth) * 2 - 1;
        var dispY = -(e.clientY / window.innerHeight) * 2 + 1;

        // 3D空間の高さと幅
        var heightOnOrigin = (Math.tan(((fov * Math.PI / 180) / 2)) * (camera.position.z - plZ) * 2)
        var widthOnOrigin = heightOnOrigin * aspRatio

        // ライトの座標
        pointLight.position.set(dispX * widthOnOrigin / 2, dispY * heightOnOrigin / 2, plZ);

      });

      /***** スマホ用 *****/
      // 触った時
      document.body.addEventListener("touchstart", function(e) {
        pointLight.intensity = 8;
        pointLight.distance = 40;
        pointLight.visible = true;
        setLightPositionForSP(e);
      });
      // 動かしている時
      document.body.addEventListener("touchmove", function(e) {
        setLightPositionForSP(e);  
      });
      // 離した時
      document.body.addEventListener("touchend", function(e) {
        pointLight.intensity = 4;
        pointLight.distance = 20;
        pointLight.visible = false;
      });

      /***************************************/
      /***** ライトの座標を設定 ここまで *****/
      /***************************************/

      /***** ウィンドウサイズ変更 *****/
      window.addEventListener("resize", function(){

        // アスペクト比
        aspRatio = window.innerWidth / window.innerHeight;
  
        // 視野角
        if (aspRatio > 1) {
          fov = 35;
        } else if (aspRatio > 0.9) {
          fov = 45
        } else if (aspRatio > 0.8) {
          fov = 50;
        } else if (aspRatio > 0.6) {
          fov = 60;
        } else if (aspRatio > 0.5) {
          fov = 70;
        } else {
          fov = 80;
        }
        camera.aspect = aspRatio;
        camera.fov = fov;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
        render();

      });

      // 描画
      renderScene();

    }
  </script>
</body>
</html>

あとがき

おばけ屋敷の特設サイトとかでどうでしょう。見づらいけど。

座標の変換が大変でしたが何とかできました。ライトが動く面の高さと幅を求めるとこなんか、自分でもよくできてるなぁと思います。はい。

でも説明がねぇ…。ヘタクソだなぁ。そこら辺の工夫が必要ですね。ありがとうございました。

おしゃれ度

★★★☆☆

Posted by ナカタ