Blender Pythonで方向ベクトルで表される方向をクォータニオンを使って実装する

# 1.目的
方向をもつオブジェクトを特定の座標(または方向ベクトルで表現される方向)に向けたい。Unityではtransform.LookAtやQuaternion.LookRotationという関数を使うと一発でできるようなのですが、Blenderにはないようなので自力で実装する必要があります。角度といえば通常はオイラー角で表すことが多い(scene viewなど)ですが、オイラー角は計算に不向きであるため、クォータニオンを用いて実装します。

# 2.環境
Blender2.93

# 3.クォータニオンについて軽く触れる
## 1.クォータニオンでできること
日本語では**四元数**と呼ばれ、**三次元空間での回転**を表現することができます。
回転を表す方法にはほかにもオイラー角、回転行列などがありますが、まず、オイラー角はパラメータは3つしかなくメモリ消費が少ないものの計算で扱いにくいです。次に回転行列は行列なので計算はしやすいですが、パラメータが多くメモリ消費が大きいです。一方のクォータニオンは回転行列の特性を引き継いでいて計算はしやすく、パラメータが4つで表されるのでメモリにもそこそこ優しいというバランスの良さが特徴です。

## 2.クォータニオンの表現
クォータニオンは**4つのパラメータをもつベクトル**として回転を表現することができます。

```math

\boldsymbol{q} = 
\left(
\begin{array}{c}
x \\
y \\
z \\
w \\
\end{array}
\right)
=
\left(
\begin{array}{c}
\lambda_x \sin \frac{\theta}{2} \\
\lambda_y \sin \frac{\theta}{2}\\
\lambda_z \sin \frac{\theta}{2}\\
\cos \frac{\theta}{2} \\
\end{array}
\right)

```
ここで、$(\lambda_x, \lambda_y,\lambda_z)$ は**回転軸をベクトル表記**したもので、$\theta$は**回転する角度**を表します。ただし、$(\lambda_x, \lambda_y,\lambda_z)$は基本的に**正規化条件**が付きます。すなわち、$\lambda_x ^2 + \lambda_y ^2 + \lambda_z ^2 = 1$を満たします。また、クォータニオンは四次元ベクトルの形式で表されていますが、**計算方法は通常のベクトルとは異なります**ので留意してください。
なぜ$\theta$ではなく$\frac{\theta}{2}$なのか、導出はどうするか、証明はどうなっているのか、計算はどうするのかなど疑問は尽きないかと思いますが、そちらは数学者に任せるとして、今回はこれを道具として使っていきます。（基本的に計算がシンプルになって都合のよい形で定義されていると思ってください。）

# 4.実装する
## 1.方向と姿勢の違いに注意
もう一度目的をおさらいすると、今回の目的はデフォルトである方向を向いているようなオブジェクトを回転させ、方向ベクトルを用いて表現できる特定の方向を向かせることです。
ここで注意したいのは方向が定まっても姿勢は一意に定まらないということです。なぜなら、方向ベクトルを回転軸にとればまだ自由に回転できるからです。（鉛筆を真下に向けながらでも自由に回転できるのと同じ。）その場合はその回転軸でもう一度回転を考える必要があります。今回の記事ではライトオブジェクトなど軸対称なオブジェクトを対象としているので方向だけを決めることにします。

## 2.実装の考え方
回転させたい物体Oに対しデフォルトで向いている方向を$\boldsymbol{a}$、目的の方向を$\boldsymbol{d}$とします。
この二つに対して、回転軸$\boldsymbol{l}$と回転角$\theta$を決定すればクォータニオンを求めることができます。以下の図を見てください。

<img width="600" alt="quaternion.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438612/0f13215e-96ce-9a3a-b7ba-50504812f10d.jpeg">

まず、回転角$\theta$は$\boldsymbol{a}$と$\boldsymbol{d}$のなす角であり、その角は内積を用いて
$$\cos\theta = \frac{\boldsymbol{a} \cdot \boldsymbol{d}}{|\boldsymbol{a}||\boldsymbol{d}|}$$
と表されます。
ここで、逆三角関数を用いると
$$\theta = \arccos (\frac{\boldsymbol{a} \cdot \boldsymbol{d}}{|\boldsymbol{a}||\boldsymbol{d}|})$$
となり、さらに$\boldsymbol{a}$と$\boldsymbol{d}$が正規化されていれば最終的に
$$\theta = \arccos(\boldsymbol{a} \cdot \boldsymbol{d})$$
を求めればよいことになります。
次に、回転軸$\boldsymbol{l}$を考えます。回転軸は$\boldsymbol{a}$、$\boldsymbol{d}$で張られる面の法線であることがわかります。法線はベクトル積を用いて
$$ \boldsymbol{a} \times \boldsymbol{d}$$
と表せ、最終的にはこれを正規化したものを使います。

## 3.いざ実装
まず、後の便利のため正規化関数を作ります。このくらいならnumpyの標準機能にありそうなものですが、意外とないものなんですね。
この関数の機能としては、引数`v`に入力されたベクトルの各成分がノルムで割られ出力されます。$\boldsymbol{v} = \boldsymbol{0}$のときはそのまま返します。
($\boldsymbol{v} \neq \boldsymbol{0}$なる任意のベクトル$\boldsymbol{v}$に対し、$\frac{\boldsymbol{v}}{|\boldsymbol{v}|}$は必ずノルムが1となります。)

```python
import numpy as np
def normalize(v):
    norm = np.linalg.norm(v)
    
    if norm == 0:
        return v
    
    return v / norm
```
次に本命の関数を作ります。

```python
import mathutils
# input: direction(vector3), output:quarternion(vector4)
def quaternion_look_rotation( direction , default = [0,0,-1]):
    # prepare
    direction = np.array(direction)
    default = np.array(default)
    # value check
    assert np.linalg.norm( direction ) > 0
    # normalize
    direction = normalize(direction)
    default = normalize(default)
    
    # calculate angle
    angle = np.arccos( np.dot( default, direction) )
    # calculate axis
    axis = normalize( np.cross(default, direction) )
    
    return mathutils.Quaternion(axis,angle)
```
まず、`direction`は向きたい方向を示す方向ベクトル、`default`はもともと向いている方向を示すベクトルです。Blenderではデフォルトは`[0,0,-1]`、つまり-Z方向となっています。

(追記)オブジェクトによって異なるようです。たとえばライトオブジェクトでは[0,0,-1]ですが、アーマチュアでは[0,0,1]になっています。このどちらかになることが予想されますが、それ以外もあるかもしれません。オブジェクトを追加した初期状態の方向を確認してみてください。
\# prepareのブロックではこれらをndarrayに変換しています。
\# value checkでは自身の座標が向きたい座標に一致しているような、このコードの対象外となる状態を`assert`で弾いています。
\# normalizeでは先に作った正規化関数を使って`direction`と`default`を正規化しています。
\# calculate angleでは先に作った回転角$\theta$の計算式、$\theta = \arccos(\boldsymbol{a} \cdot \boldsymbol{d})$を計算しています。angleは角という意味です。
\# calculate axisでは先に作った計算式、$\boldsymbol{a} \times \boldsymbol{d}$を用いて回転軸を求めた後に、正規化関数で正規化しています。axisは軸という意味です。
最後に、クォータニオンを返せば完成です。3.2節で書いたように一つ一つ代入して四次元ベクトルを返しても構いませんが、ここでは、bpy(Blender Python API)の標準ライブラリである`mathutils`の`mathutils.Quaternion()`クラスを使うと簡単です。引数に(axis,angle)を入れるだけでクォータニオンを作ってくれます。

:::note warn
注意
もし、値を一つ一つ代入して四次元ベクトルを返す場合は要素の順番に注意してください。
上の3.2節の説明では(x,y,z,w)の順番で書きましたが、Blenderでは(w,x,y,z)の順番で扱います。
:::

## 実行してみる
動かす対象のオブジェクトを作り、方向ベクトルを求めます。
方向ベクトルは `ターゲットの座標 - 自身の座標` で求められますから、それを引数に入れて関数が使えます。

```python
import bpy
#向きたい座標を決める。ここでは[3,2,1]
target_position = [3,2,1]
target_position = np.array(target_position)
#現在の物体の座標を取る
obj = bpy.context.object
obj_position = obj.location
obj_position = np.array(obj_position)
#回転モードをクォータニオンに変更
obj.rotation_mode = 'QUATERNION'
#方向ベクトルを取る
direction = target_position - obj_position
#関数を呼び出してクォータニオンをいれる
obj.rotation_quaternion = quaternion_look_rotation(direction)

```
:::note warn
注意
回転モードをクォータニオンに変更するのは超重要です。これを忘れるとobj.rotation_quaternionにクォータニオンを代入しても回転が反映されません。
:::

# 5.参考文献
ここまでお読みいただきありがとうございました。最後に参考文献を紹介します。

https://qiita.com/hibit/items/d457ca6f9091fbb80ce6

今回の記事は、上の記事でUnityを使って行っていることをblenderに焼き直して作られています。

また、クォータニオンの理論的な部分は下の記事を参考にしています。

https://qiita.com/drken/items/0639cf34cce14e8d58a5#1-%E3%82%AF%E3%82%A9%E3%83%BC%E3%82%BF%E3%83%8B%E3%82%AA%E3%83%B3%E3%82%92%E7%94%A8%E3%81%84%E3%81%A6%E3%82%84%E3%82%8A%E3%81%9F%E3%81%84%E3%81%93%E3%81%A8

どちらも非常にわかりやすい記事だと思うので皆さんもぜひ参照してみてください。

また、ここで個人的に謝辞を述べたいと思います。両記事の作者さま、わかりやすい記事をありがとうございました。両記事ともにとても役に立ちました！