マウスを追ってくるオブジェクトをThree.jsで作ってみた

# マウスを追ってくるオブジェクトをThree.jsで作ってみた

## 前に書いた記事

[WebGL Advent Calendar 2018 - Qiita](https://qiita.com/advent-calendar/2018/webgl) １５日目の記事です（遅刻）

以前こんな記事を投稿しました。

[Google PixcelのサイトがかっこよすぎたのでWeb上での3Dオブジェクトの扱い方に少しだけ触れてみた [three.js] – 忘れっぽいLOG](https://spadeloves.tk/index.php/2018/09/26/threejs_prevew/ "Google PixcelのサイトがかっこよすぎたのでWeb上での3Dオブジェクトの扱い方に少しだけ触れてみた [three.js] – 忘れっぽいLOG")
タイトルの通り、Pixcel 3 のサイトがかっこよかったので、`Three.js`の使い方を簡単に学んでみるよ！という記事です。
この記事では、Three.js公式の入門記事に従い、回転するキューブを描画するだけのページを作成しました。

## ここから本題

今回は、例のサイトにより近づけるため、**マウスを追いかけるように回転するキューブ**を作成しました！
顔がマウスを追ってくるとか、目がマウスを追ってくるとか、そんなことに使えるのでは…多分…


## つかったもの

Three.jsとVue.jsを使って実装をしました。

## 実装方法

まずは、ワイヤーのキューブを描画します。
そもそも「Three.jsがまず全くわからん！」という方は[こちら](https://spadeloves.tk/index.php/2018/09/26/threejs_prevew/ "Google PixcelのサイトがかっこよすぎたのでWeb上での3Dオブジェクトの扱い方に少しだけ触れてみた [three.js] – 忘れっぽいLOG")

### 下準備と初期描画

Vue.jsで処理を行うため、書くオブジェクト、パラメータをまずは宣言します。

```JS
export default {
  data () {
    return {
      //３Dオブジェクト描画用のThree.jsのパラメータ
      scene : new THREE.Scene(), 
      camera : new THREE.PerspectiveCamera( 100, window.innerWidth/window.innerHeight, 0.1, 1000 ), 
      renderer : new THREE.WebGLRenderer(), 
      geometry : new THREE.BoxGeometry( 4, 4, 4 ), 
      material : new THREE.MeshBasicMaterial( { color: 0x563d7c, wireframe: true } ),
      
      canvasWrap : 0,        //描画ターゲットのDOM格納用
      cube : 0,              //正方形のオブジェクト格納用
      delay : 0.001,         //遅延処理用パラメータ（後述）
      loop_counter: 0,       //遅延処理用ループカウンター（後述）
      move_target_x : 0,     //オブジェクトが向くべき角度x
      move_target_y : 0,     //オブジェクトが向くべき角度y
    }
  },
```

そして、初期描画処理を行います。以下のメソッドはすべてVueの`methods`として定義します。

```HTML
<div class="container">
  <div id="canvas_wrap" class="canvas_wrap"></div>
</div>
```

```JS
//初期描画
firstdraw : function(){
  this.cube = new THREE.Mesh( this.geometry, this.material );
  this.cube.position.set(0,0,0);
  this.scene.add( this.cube );
  this.camera.position.z = 10;
  this.canvasWrap = document.getElementById("canvas_wrap");
  this.renderer.setSize( this.canvasWrap.clientWidth, this.canvasWrap.clientHeight);
  this.canvasWrap.appendChild( this.renderer.domElement );
  this.renderer.render( this.scene, this.camera );
  this.animate(); //描画
},
//アニメーション用再描画処理
animate : function(){
  requestAnimationFrame( this.animate )
  this.renderer.render( this.scene, this.camera )
}
```

これでとりあえずワイヤーの正方形が描画されました。

<img width="1440" alt="スクリーンショット 2018-12-16 19.15.21.png" src="https://qiita-image-store.s3.amazonaws.com/0/182837/c89c8386-da40-2aa2-73cd-8e9f41e435b7.png">


### ポインタの座標取得と回転角度の処理

つぎに、ポインタの座標に合わせて、オブジェクトの回転角度を変える処理を作成します。
マウスの座標と、描画する`canvas`要素の座標から、実際にキューブが向くべき角度を計算します。

```JS
//マウスに追従して回転角度を設定
changeBoxDirection : function(){
  //canvasの座標取得
  var rect = this.canvasWrap.getBoundingClientRect() 

  //回転角度の計算　event.screenX,Yでポインタの座標を取得できます
  var cube_rotation_y = (event.screenX - this.canvasWrap.clientWidth/2 - rect.left) / 360
  var cube_rotation_x = (event.screenY - this.canvasWrap.clientHeight/2 - rect.top) / 360

  // キューブの回転角度に代入
  this.cube.rotation.x= cube_rotation_x
  this.cube.rotation.y= cube_rotation_y
  this.animate(); //描画
},
```

HTML側には、`mousemove`のイベントを取得するよう追記します。

```HTML
<div class="container">
  <div id="canvas_wrap" class="canvas_wrap" @mousemove="changeBoxDirection()"></div>
</div>
```

これで、ポインタがcanvas要素上で動くと、オブジェクトがマウスを追従するようになりました。

![画面収録 2018-12-16 19.14.32.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/182837/0976fa14-f8b7-fb31-4b41-39ae473895df.gif)


これでやりたいことはできたのだけど、なにか違う…
もしこのキューブがキャラクターの顔だったら…？

ぼく「遅延が必要なのか！！！」

### いい感じに遅延させる

というわけで、キューブの追従に **遅延** を行うように処理を追加します。

#### 遅延処理の考え方

遅延処理のイメージは簡単にこんな感じ
- 現在の角度と、目的の角度に大きく差があれば早く動く
- 目的の角度に近づくほど、ゆっくり動く

そのため、処理としては、 **現在の角度と目的の角度の差が回転速度に比例する処理** を作成すればいいことになります。

具体的な計算式はこんな感じ
`新しいキューブの角度 = (目的の回転角度 - 現在のキューブの角度) * 遅延定数`

この計算をループで呼び出すことで、遅延する角度変更ができるようになります。

#### ソースに落とし込みます

```JS
//マウスに追従して回転角度を設定
changeBoxDirection : function(){
  var rect = this.canvasWrap.getBoundingClientRect() //canvasの座標取得

  //回転角度の計算
  var cube_rotation_y = (event.screenX - this.canvasWrap.clientWidth/2 - rect.left) / 360
  var cube_rotation_x = (event.screenY - this.canvasWrap.clientHeight/2 - rect.top) / 360

  // キューブの角度に代入していたところを、ターゲットへの代入に変更
  this.move_target_x = cube_rotation_x
  this.move_target_y = cube_rotation_y
  this.goDefaultLoop(); //遅延処理用ループの呼び出し
},
//遅延処理のためのループ
goDefaultLoop : function () {
  var self = this
  setTimeout(function () {
    self.goDefaultCalclate() //キューブの角度計算処理の呼び出し
    //無限ループを防ぐためのループ回数を制限
    self.loop_counter++;  
    if (self.loop_counter < 100) { 
      self.goDefaultLoop()
    }else{
      self.loop_counter = 0
    }                      
  }, 60)//60ミリ秒ごとに処理
},  
goDefaultCalclate:function(){
  //次にキューブに加算する角度の初期化
  var velocityX = 0
  var velocityY = 0
  //キューブに加算する角度の計算
  velocityX = (this.move_target_x - this.cube.rotation.x) * this.delay
  velocityY = (this.move_target_y - this.cube.rotation.y) * this.delay
  //キューブの角度に加算
  this.cube.rotation.x += velocityX
  this.cube.rotation.y += velocityY
},
```

上記の処理を行うことで、キューブが**生き物っぽく**ついてくるようになりました！
再描画処理は常に動き続けているので、`this.cube.rotation`を変更するだけで描画されます。


### おまけ：マウスが外れたら真ん中に戻る

このままだと、ポインターが`canvas`から外れてしまうとその角度を向いたままになるので、ポインターが外れたら真ん中に戻る処理を追加します。

`mouseout`イベントを追加

```HTML
<div class="container">
  <div id="canvas_wrap" class="canvas_wrap" @mousemove="changeBoxDirection()" @mouseout="defaultBoxDirection()"></div>
</div>
```

```JS
//マウスが外れたときにもとに戻る処理
defaultBoxDirection : function(){
  //ターゲットを(0,0)に設定
  this.move_target_x= 0
  this.move_target_y= 0
  //遅延用ループの呼び出し
  this.goDefaultLoop();
},
```

これでマウスが外れたら真ん中に戻るようになりました。

## つくってわかったこと、よかったこと

- Vue.jsとアニメーション処理の相性の良さ
- Three.jsの簡単さ
- **作りたいと思ったものを作る喜び**（これ大事）
- 自分のソースのわかりにくさ…精進します…

## 課題

- [ ] `mousemove`の度にループが呼び出されるので、動かし続けるとだんだんキューブが早くなる（キャンセル処理を入れないと…）
- [ ] スマホ対応するために、スマホの回転と連動するようにしたい

## まとめ

ページを見てもらえればわかるのですが、これは自分のポートレートサイトを作ろうとしてできた副産物（？）です。

ぼく「なんかインパクトのあるものを置かないと覚えてもらえないよな…」
ぼく「3Dオブジェクトとかめっちゃ目立つのでは…？」

という経緯で作ってみました。自分の顔を3Dオブジェクトにして置いておいたら、ポートレートサイトとしてすごいインパクトありますよね。 ~~誰か作って~~

ちなみにポートフォリオサイトはまだ出来上がってません。（わらい）
こんなに長い記事を書いたのは初めてで、読みにくくなってしまったかもしれません。
ここまで読んでいただき、ありがとうございました！
もし改善点や、こうしたらもっと簡単にできる、などありましたら教えてください！！

では！