three.js + react-springで使えるOrbit(Spherical-ish)な補間を作った

[useOrbitInterpolation.ts](https://gist.github.com/nariakiiwatani/e045d905f5463ff2e6a4f85c628ed7c7)

three.jsでOrbitControlしているカメラをプログラムから動かしたい場合の良い感じの補間が欲しかったので作った。
基本的なモチベーションとしては、OrbitControlをマウスやタッチでグリグリしてる時と同じ感じの補間が欲しいということなので、要件を書き下すと

- targetに注目したまま
- targetとの距離を保って
- 球面上を移動する

という補間になりそう。

![orbitIP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16556/5d99b91f-e310-9993-0aa3-2ee557319307.png)


カメラの移動先が球面上にない場合も考慮せねばなので

- targetに注目したまま
- targetとの距離を補間しつつ
- 目的の位置まで移動する

というタスクになりそう。

![orbitIP2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16556/5bd8d068-1b9b-3314-bfe1-38c6f1a49409.png)

同時にtargetが動く場合も同じルールでいけそうなのでこの方針で実装する。

```ts:雰囲気で書くとこんな感じ
const result = (from, to)=> {
	 // targetは素直に補間
	const target = interpolation(from.target, to.target)
	// 距離も素直に補間
	const distance = interpolation(distance(from.position, from.target), distance(to.position, to.target))
	// 向きは球面補間
	const direction = sphericalInterpolation(direction(from.position, from.target), direction(to.position, to.target))
	// positionを逆算
	const position = target - direction*distance
	return { position, target, distance }
}
```

で、これをreact-springを使って書くとこんな感じ。冒頭のリンクと（今後編集されない限り）同じ内容です。
カメラの補間に使うものなのでupベクトルの計算も入れてます。

```ts:useOrbitInterpolation.ts
import { useCallback } from 'react'
import * as THREE from "three"
import { useSpring } from "react-spring"

export interface OrbitParam {
	position: THREE.Vector3,
	target: THREE.Vector3,
	up: THREE.Vector3,
	fov: number,
	zoom: number
}

const align = (src: THREE.Vector3, guide: THREE.Vector3): THREE.Vector3 => {
	src.cross(guide.normalize())
	return guide.cross(src)
}

export const useOrbitInterpolation = ({
	from, to, reset = false, onStart, onFrame, onRest, immediate = false
}: {
	from?: OrbitParam,
	to: OrbitParam,
	reset?: boolean,
	onStart?: () => void,
	onFrame?: (OrbitParam) => void,
	onRest?: () => void,
	immediate?: boolean
}) => {
	const makeParam = src => {
		const sub = new THREE.Vector3().subVectors(src.target, src.position)
		return {
			target: to.target.toArray(),
			heading: sub.clone().normalize().toArray(),
			distance: sub.length(),
			up: to.up.toArray(),
			fov: to.fov,
			zoom: to.zoom
		}
	}
	const [value, set] = useSpring(() => ({
		from: from && makeParam(from),
		to: makeParam(to),
		reset,
		onFrame: props => {
			const target = new THREE.Vector3().fromArray(props.target)
			const position = target.clone().addScaledVector(new THREE.Vector3().fromArray(props.heading), -props.distance)
			const up = align(new THREE.Vector3().fromArray(props.up), new THREE.Vector3().subVectors(target, position))
			onFrame({ position, target, up, fov: props.fov, zoom: props.zoom })
		},
		onStart,
		onRest
	}))
	const reInterpolate = useCallback(({ from, to, reset }: {
		from?: OrbitParam,
		to: OrbitParam,
		reset?: boolean
	}) => {
		set({
			from: from && makeParam(from),
			to: makeParam(to),
			reset,
		})
	}, [set])
	return reInterpolate
}
```

せっかくhook的なものを作ったので、(react-springとかthreeとかへの依存を切る形に編集してから)公開したいけど

- d.ts 書いたことないし
- npmへの公開の仕方しらんし
- 十分テストできてないし

ReactもTypeScriptもthree.jsも初心者な状態では心理的なハードルが高すぎて折れそう・・・。
業務が落ち着いて心に余裕ができたらやります。。

ちなみに（ほぼ）同じことをopenFrameworksのカメラでやっているのがこちらになります。
[ofxKeyframeTween](https://github.com/nariakiiwatani/ofxKeyframeTween/)
[該当箇所](https://github.com/nariakiiwatani/ofxKeyframeTween/blob/master/src/interpolation/ofxTweenTypeInterpolateNode.h#L54-L61)
カメラだけじゃなくいろんな型のいろんな補間を柔軟に扱えるaddonなんですが、あんまり利用者いなそうなので布教したいやつです。
また記事を書くとちょっと良いのかな？そのうち。