Spline曲線に沿ってカードを動かす

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="zYzNZrM" data-user="112KA" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/112KA/pen/zYzNZrM">
  Frame Graphic with Spline path</a> by iizuka (<a href="https://codepen.io/112KA">@112KA</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>


↑３D空間上のSpline曲線に沿ってオブジェクトを動かすというのを実装する機会があったので、記事化しておく。

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="MWpVOer" data-user="112KA" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/112KA/pen/MWpVOer">
  Spline path</a> by iizuka (<a href="https://codepen.io/112KA">@112KA</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

1スプライン1カードのサンプルはこれ↑
白線 - Spline曲線
緑点 - Spline分割点
緑線 - Splineの外向きベクトル

# 実装手順

## 1. Spline曲線を分割
THREE.CatmullRomCurve3では、getPointsで分割してくれる
サンプルでは25分割している

```
let curve = new THREE.CatmullRomCurve3([...])
this.points = curve.getPoints(N_DIVIDE);
```

## 2. 各分割点で、外向きベクトルを算出
下図、点Bでの外向きベクトルを求めるために、線分ABと線分CBを足してnormalizeしている
![img1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/207875/f544f9e6-43ee-a761-2e55-8da737613b3a.jpeg)


## 3. 各分割点で、外向きベクターの方向に回転するためのQuaternionを算出
山場の実装。
参考にしたのは[このページ](http://marupeke296.com/DXG_No16_AttitudeControl.html)
方向をあわせる回転角度が時計回りかどうかを求める必要があったのがわからなくて、そこでほまって時間かかった。

### カード（ローカルオブジェクト）のベクトル
姿勢を合わせるためのカードの法線(0,0,1)と進行方向(0,1,0)

```
let cardNormal = new THREE.Vector3(0, 0, 1)
  , cardDirection = new THREE.Vector3(0, 1, 0)
```
### 姿勢合わせ回転
```
verticalVector = cardNormal.clone().cross(pB.outward)  //カード面の法線と点Bの外向きベクトルに垂直なベクトル
verticalVector.normalize()
radians = Math.acos(cardNormal.dot(pB.outward))  //回転角度

attitudeQuaternion.setFromAxisAngle(verticalVector, radians)
```
### 方向合わせ回転
```
//点Bの外向きベクトルをカード平面に射影
//参考) http://www.thothchildren.com/chapter/5b670e772787593b86356103
pB.targetDirection = lineBC.clone()
pB.targetDirection.sub(pB.outward.clone().multiplyScalar(pB.outward.dot(lineBC)))
pB.targetDirection.normalize()
let localDirection = cardDirection.clone()

localDirection.applyQuaternion(attitudeQuaternion)

radians = Math.acos(pB.targetDirection.dot(localDirection))  //回転角度
      
//このときの回転角度が時計回りかどうかを調べる
localDirection.cross(pB.targetDirection);
let code = localDirection.dot(pB.outward) > 0 ? 1 : -1;

//点Bの外向きベクトルを軸に回転
directionQuaternion.setFromAxisAngle(pB.outward, code*radians)
```

### 姿勢と方向、それぞれのQuaternionを1つのQuaternionに合成
```
pB.quaternion = new THREE.Quaternion()
pB.quaternion.multiply(directionQuaternion)
pB.quaternion.multiply(attitudeQuaternion)
```


## 4. 各分割点の”位置”、”Quaternion”の補間値を移動させたいオブジェクトに設定する
- THREE.Vector3のlerpVectors
- THREE.QuaternionのslerpQuaternions

で、”位置”、”Quaternion”をそれぞれ補間してくれるので、それをカードオブジェクトのmatrixに設定する

終わり！

# 参考
- http://marupeke296.com/DXG_No16_AttitudeControl.html
- http://www.thothchildren.com/chapter/5b670e772787593b86356103