エフアンダーバー

個人でのゲーム開発

【Unity】 Quaternion API解説

Quaternion(クォータニオン、四元数)は回転操作において重要な数です。 しかし、回転という操作がなかなか想像しづらいことや、Quaternionの扱いに少し癖があることなどから、 結構ハマりやすいところだと思います(というか、自分がよくハマります)。 そこで、UnityにおけるQuaternionのAPIとその注意点についてまとめてみました。

この記事ではQuaternionの理論についてはほとんど触れません。 Quaternionの理論について知りたい場合には過去記事や他所の記事を参照してください。

www.f-sp.com

Quaternion API

とりあえずAPIの一覧。

重要度は主観です。 必要性というよりは有用性や利用頻度を重視しててきとーに決めました。

名前 説明 重要度
identity 恒等回転(読み込み専用)。 ★★★★★
eulerAngles 回転をオイラー角として取得。 ★★☆☆☆
this[int] インデクサ。 ★☆☆☆☆
w Quaternionのw要素。 ★☆☆☆☆
x Quaternionのx要素。 ★☆☆☆☆
y Quaternionのy要素。 ★☆☆☆☆
z Quaternionのz要素。 ★☆☆☆☆
Quaternion コンストラクタ。 ★☆☆☆☆
Set w,x,y,z要素に値を設定。 ★☆☆☆☆
SetFromToRotation ある方向からある方向への回転に設定。 ★☆☆☆☆
SetLookRotation 指定した向きを向くための回転に設定。 ★☆☆☆☆
ToAngleAxis 回転の角度と回転軸を取得。 ★★☆☆☆
Angle 二つのQuaternionの間の角度を取得。 ★★★☆☆
AngleAxis 角度と回転軸を指定して回転を作成。 ★★★★★
Dot Quaternionの内積を計算。 ★★☆☆☆
Euler オイラー角表現から回転を作成。 ★★★★☆
FromToRotation ある方向からある方向への回転を作成。 ★★★★☆
Inverse 逆Quaternionを作成。 ★★★★★
Lerp 線形補間。 ★★★☆☆
LerpUnclamped [0,1]に定義域を制限しない線形補間。 ★★☆☆☆
LookRotation 指定した向きを向くための回転を作成。 ★★★★☆
RotateTowards 最大回転量を制限した補間。 ★★★☆☆
Slerp 球面線形補間。 ★★★★☆
SlerpUnclamped [0,1]に定義域を制限しない球面線形補間。 ★★☆☆☆
*演算子 回転の合成。 ★★★★★
*演算子 回転の適用。 ★★★★★
==演算子 等値判定。 ★★★☆☆

解説

重要度 ★★★★★

identity

identityは恒等回転(無回転)を表すQuaternionを取得するプロパティです。

移動操作における零ベクトルのようなもので、初期状態の回転値を表すためによく使います。

「無回転」とはいっても、その値は0ではないためdefault(Quaternion)とは値が違います。 C#による初期化処理に任せると無効な回転になってしまうため、 Quaternionを初期化する必要がある場合にはしっかりとQuaternion.identityを代入します。

// 複数の回転の合成をきれいに書きたいときにも便利
Quaternion rot = Quaternion.identity;
rot = Quaternion.AngleAxis(15.0f, Vector3.right) * rot;
rot = Quaternion.AngleAxis(45.0f, Vector3.up) * rot;
...

AngleAxis

AngleAxisは回転角度と回転軸からQuaternionを作成する関数です。

これはQuaternionを作成する最も基本的な方法です。 どの向きを軸としてどれくらいの角度回転させるかを指定します。

Unityは左手座標系のため、回転方向は軸の正方向を向いて反時計回りです。 左手の親指だけを立てて軸の方を向けたときに、親指に対してそれ以外の指が向いている方向です(左ねじ)。 ちなみに左手座標系は、左手をピストルの形にしてさらに中指を垂直に立てたとき、 親指をX軸、人差し指をY軸の正方向に向けると、中指がZ軸の正方向を向くような座標系を言います(右手でやると逆向きになります)。

QuaternionのAPI全体に言えることですが、角度の指定が弧度法(ラジアン)ではなく度数法(度)なことに注意が必要です。 特にベクトルの内積や外積の結果から逆三角関数を用いて得た角度などを利用する場合には、 Mathf.Rad2Degを乗算して度数法に直すことを忘れないようにしてください。

また、Quaternionによる回転は原点中心の回転です。 他の点を中心として回転させたい場合には、その点が原点になるように平行移動した後に回転を適用し、再度同じ量だけ逆向きに平行移動します。

// 点(1,2,3)を(1,1,1)を中心としてY軸周りに180度回転
Vector3 p = new Vector3(1f, 2f, 3f);
p -= Vector3.one;
p = Quaternion.AngleAxis(Mathf.PI * Mathf.Rad2Deg, Vector3.up) * p;
p += Vector3.one;
Debug.Log(p); // (1.0, 2.0, -1.0)

Inverse

Inverseは逆Quaternionを作成する関数です。

逆Quaternionは逆行列のようなもので、元のQuaternionの回転を打ち消すような回転、つまりは同じ軸で逆向きの回転を表します。 そのため、元のQuaternionと逆Quaternionを合成すると、回転を打ち消し合ってQuaternion.identityの値と一致します (ただし、計算誤差があるため完全に一致するとは限らない)。

// 逆Quaternionと元のQuaternionを合成
Quaternion rot = Quaternion.AngleAxis(60.0f, Vector3.forward);
Quaternion q1 = rot * Quaternion.Inverse(rot);
Quaternion q2 = Quaternion.Inverse(rot) * rot;

Debug.Log(Quaternion.identity); // (0.0, 0.0, 0.0, 1.0)
Debug.Log(q1);                  // (0.0, 0.0, 0.0, 1.0)
Debug.Log(q2);                  // (0.0, 0.0, 0.0, 1.0)

*演算子(QuaternionQuaternion

Quaternion同士の間に定義される*演算子はQuaternionの乗算を表します。

Quaternionは乗算によって合成することができます。 すなわち、乗算によって2つのQuaternionによる二段階の回転を、同一の結果を生む1つのQuaternionによる一段階の回転にすることができます。 複雑な組み合わせによる回転も、あらかじめ回転同士を合成しておけば、適用の処理が一回で済むので高速化が可能です。

Quaternionの乗算は行列と同じで、左右どちらから掛けるかによって計算結果が異なります(交換法則が成り立たない)。 つまり、二つの回転のどちらを先に適用するかという問題があるわけです。 Unityでは、右側に掛けたものから順に回転が適用されるというルールがあるため、先に適用したい回転を右側に書きます。

C#には*=演算子という乗算と代入を同時に行う演算子がありますが、 言語仕様上この演算子の左辺値は乗算時にも左辺値となるため、 この演算子を用いると後から掛けたものほど先に適用されるというわかりづらい記述になります。 そのため、Quaternionに対しては*=演算子を使わないことをおすすめします。

注意点として、先に適用する回転によって、後に適用される回転の軸が回転することはありません。 回転の順番を考える時にはこのことを念頭に置いておくといいと思います。

// 乗算の順序で結果が変化
Quaternion rot1 = Quaternion.AngleAxis(90.0f, Vector3.forward);
Quaternion rot2 = Quaternion.AngleAxis(90.0f, Vector3.up);

Debug.Log(rot2 * rot1 * Vector3.one); // (1.0, 1.0, 1.0)
Debug.Log(rot1 * rot2 * Vector3.one); // (-1.0, 1.0, -1.0)

*演算子(QuaternionVector3

QuaternionVector3の間に定義される*演算子は座標に対して回転を適用します。

ベクトルに対して*演算子を適用する場合には、Quaternionを必ず左側に書く必要があります *1。 Unityで右側から掛けたものから順に回転が適用されるのはこの仕様のためです (ベクトルの近くに掛けられたものほど先に適用される)。

余談ですが、この操作は*演算子で表されますが、数学的な意味では乗算ではありません。 そのため、Unity以外では*演算子ではなく関数で表されることもあります。

// 点(1,1,1)をY軸周りに30度回転
Quaternion rot = Quaternion.AngleAxis(30.0f, Vector3.up);
Vector3 p = Vector3.one;

Debug.Log(rot * p); // (1.4, 1.0, 0.4)

// 逆には書けない
// Debug.Log(p * rot);

// 実際にはこんな感じの計算
Quaternion q = rot * new Quaternion(p.x, p.y, p.z, 0f) * Quaternion.Inverse(rot);
Debug.Log(new Vector3(q.x, q.y, q.z)); // (1.4, 1.0, 0.4)

重要度 ★★★★☆

Euler

Eulerはオイラー角表現からQuaternionを作成する関数です。

オイラー角は各座標軸周りの回転角度による回転の表現方法です *2。 エディタ上でTransformコンポーネントのRotationの表示に利用されている形式です。 キャラクター等の前後左右がはっきりとしたモデルを回転させる際には、 任意軸回転よりもオイラー角の方が直感的なためよく使われます。

オイラー角では各座標軸周りで計三回の回転を行うため、回転の順番が重要になります。 UnityではZ軸、X軸、Y軸の順番を採用しています。 Unityは正面をZ軸正方向、頭上をY軸正方向と定めているため、この回転順が最も直感的になります。

例えば戦闘機による3DSTGを仮定すると、 上下と左右それぞれに機首を向ける回転とアクロバット飛行的なローリングの三軸回転を考えるのがわかりやすいと思います。 このとき戦闘機が初期状態においてY軸正方向を上側にZ軸正方向を向いているとすると、 ローリング(Z軸)、上下方向(X軸)、左右方向(Y軸)の順番でないと想定通り回転しません *3

オイラー角を使う上での注意点としてジンバルロックの問題があります。 ジンバルロックは三軸回転において、二番目の回転軸(UnityではX軸)が90度回転したときに他の二つの回転方向が一致してしまう現象です。 回転軸が二つしかないのと同じ状態なので回転の自由度が減少します。 もともとジンバルという姿勢制御装置の部品で起こる現象で、ある向きに対して回転できなくなってしまう(ロックされている)状態を指します。 とはいえこれは物理的にうまく回転しないという話であって、コンピュータ上では別の回転値を与えれば当然回転します。

では何が問題かというと、回転軸が重なることで同じ姿勢を表す回転値が無数に存在してしまうことです。 これにより、オイラー角表現における値が大きく異なるにもかかわらず、姿勢が非常に近い回転が存在してしまいます。 このような二つの回転をオイラー角表現のまま補間してしまうと、姿勢が近いにも関わらず大きく変化しながら補間してしまいます。

Quaternionは任意の姿勢をほぼ一意に表せる(過去記事参照)ため、 オイラー角であってもQuaternionに変換してから補間する場合にはジンバルロックの問題は発生しません。 ただし、オイラー角による補間とQuaternionによる補間の結果は一致しないため、 アニメーションに多少の変化が出る可能性があります。

// Quaternionとオイラー角の比較
Vector3 e1 = new Vector3(-90.0f, 200.0f, -200.0f);
Vector3 e2 = new Vector3(-90.0f, 0.0f, 0.0f);
Vector3 e3 = new Vector3(-80.0f, 0.0f, 0.0f);
Quaternion q1 = Quaternion.Euler(e1);
Quaternion q2 = Quaternion.Euler(e2);
Quaternion q3 = Quaternion.Euler(e3);
Quaternion rotA = Quaternion.Slerp(q1, q3, 0.5f);
Quaternion rotB = Quaternion.Euler(Vector3.Lerp(e1, e3, 0.5f));

Debug.Log(e1 == e2); // False
Debug.Log(q1 == q2); // True
Debug.Log(q1 * Vector3.forward);   // (0.0, 1.0, 0.0)
Debug.Log(q3 * Vector3.forward);   // (0.0, 1.0, 0.2)
Debug.Log(rotA * Vector3.forward); // (0.0, 1.0, 0.1)
Debug.Log(rotB * Vector3.forward); // (0.1, 1.0, 0.0)

LookRotation

LookRotationは指定した向きを向くためのQuaternionを作成する関数です。

LookRotationにはforwardupwardsという二つの向きを指定します。 forwardは正面方向を、upwardsは上方向を表し、 結果のQuaternionを適用するとZ軸の正方向がforwardの向きに、Y軸の正方向がupwards寄りの向きになるように回転します。 なぜ『寄り』なのかというと、Z軸とY軸が直交していなければならない一方で、forwardupwardsが直交しているとは限らないからです *4

LookRotationではまずforwardの向きをZ軸正方向と決めます。 その後、upwardsforwardの外積の向き(二軸に直交する向き)をX軸正方向とします。 最期にZ軸とX軸に直交する向きをY軸として(左手座標系となるように正方向を決める)、その座標系への回転を表すQuaternionを返します。 forwardupwardsが平行だった場合には、外積が零ベクトルになるためエラーとなります。 upwardsは省略が可能で、その場合にはY軸正方向(0,1,0)が使われます。

LookRotationは対象が回転適用前にY軸正方向を上にしてZ軸正方向を向いているという仮定のもとでうまく動作する関数です。 使用の際には前提を満たしているかどうか確認する必要があります。

// 上下逆さまでX軸正方向を向く
Vector3 forward = Vector3.right;
Vector3 upwards = Vector3.down;
Quaternion rot = Quaternion.LookRotation(forward, upwards);

Debug.Log(rot * Vector3.forward); // (1.0, 0.0, 0.0)
Debug.Log(rot * Vector3.up);      // (0.0, -1.0, 0.0)

// 回転後の各座標軸
Vector3 z = forward;
Vector3 x = Vector3.Cross(upwards, z).normalized;
Vector3 y = Vector3.Cross(z, x);

Debug.Log(x == rot * Vector3.right);   // True
Debug.Log(y == rot * Vector3.up);      // True
Debug.Log(z == rot * Vector3.forward); // True

FromToRotation

FromToRotationはある方向からある方向へと回転させるQuaternionを作成する関数です。

FromToRotationでは開始方向と終了方向を指定することで、開始方向を終了方向へと向ける回転を表すQuaternionを返します。 ここで問題となるのが、開始方向を終了方向へと向ける回転というのは無数に存在するということです。 この関数で取得できるのはそのうち最も回転角度の小さいものになります。

使い方が簡単なので、使い勝手がよさそうに見えますが過信するとバグに陥りやすい関数です。 例えば、キャラクターにある方向を向かせたいからといって使ってしまうと、キャラクターが斜めに傾いたりします。 LookRotationを見ればわかる通り、モデルの向きというのは正面方向だけでは決まらないのです。

f:id:fspace:20170811193320g:plain
同じ開始方向と終了方向の回転は無数に存在

// (1,1,1)の方向を向く回転
Quaternion rot1 = Quaternion.FromToRotation(Vector3.forward, Vector3.one);
Quaternion rot2 = Quaternion.LookRotation(Vector3.one);

// 正面方向は同じ
Debug.Log(rot1 * Vector3.forward); // (0.6, 0.6, 0.6)
Debug.Log(rot2 * Vector3.forward); // (0.6, 0.6, 0.6)

// それ以外は異なる
Debug.Log(rot1 * Vector3.right);   // (0.8, -0.2, -0.6)
Debug.Log(rot2 * Vector3.right);   // (0.7, 0.0, -0.7)
Debug.Log(rot1 * Vector3.up);      // (-0.2, 0.8, -0.6)
Debug.Log(rot2 * Vector3.up);      // (-0.4, 0.8, -0.4)

Slerp

Slerpは二つのQuaternionの間を球面線形補間(Spherical Linear intERPolation)する関数です。

球面線形補間は二つのQuaternionとその影響度の割合から中間的な回転を表すQuaternionを作成します。 「球面」という言葉がついているので何か特殊な補間のように思えるかもしれませんが、Quaternionの回転としてはこれが最も基本的な補間です。 回転を表すQuaternionは球面上にあるためそれらを線形補間しようとすると球面線形補間となるのです(詳細は過去記事参照)。

影響度の割合は[0,1]の範囲の値を与えることで指定します。 0は先に指定したQuaternionの影響度100%、1は後に指定したQuaternionの影響度100%です。 中間値を与えるとそれらの影響度が線形に(比例するように)変化していきます。 また0より小さい値を与えた場合には0、1より大きい値を与えた場合には1として扱われます(clamp)。

// 角度について線形に補間される
Quaternion a = Quaternion.AngleAxis(-90.0f, Vector3.up);
Quaternion b = Quaternion.AngleAxis(90.0f, Vector3.up);
Quaternion c = Quaternion.AngleAxis(45.0f, Vector3.up);
Quaternion q1 = Quaternion.Slerp(a, b, 0.75f);
Quaternion q2 = Quaternion.Slerp(b, a, 0.25f);
Quaternion q3 = Quaternion.Slerp(a, b, 2.0f);

Debug.Log(q1 == c); // True
Debug.Log(q2 == c); // True
Debug.Log(q3 == b); // True

重要度 ★★★☆☆

Angle

Angleは二つのQuaternionの間の角度を取得する関数です。

二つのQuaternionの角度と言われてもいまいちピンとこないかもしれませんが、 二つの回転がどれくらい似ているかの尺度だと考えると使い道が想像しやすいかもしれません。 角度が小さいほど似ている回転になります。 回転合成の繰り返しによって特定の回転にぴったり一致するということはほぼあり得ないため、 ある向きに一致するという判定をしたいときには、等値比較よりも、角度が閾値以下であるかどうかを判定した方がいい場合があります。

一般に二つのQuaternionのなす角度というと、正規化したQuaternionの内積の結果に逆余弦関数を適用した値を指しますが、この関数はその2倍の値を返します。 3次元空間の回転に用いるQuaternionは指定した回転角度の半分の値を用いるため(過去記事参照)、 2倍して指定した回転角度と同じになるようにしているものと思われます。 つまり、正確にはQuaternion間の角度ではなく、片方の姿勢からもう片方の姿勢へと回転させる際の回転角度の絶対値を表します。

この関数によって取得した角度は弧度法ではなく度数法なので、三角関数の引数などに用いる場合にはMathf.Deg2Radを掛ける必要があります。

// 片方の姿勢からもう片方の姿勢へと回転させる際の回転角度と一致
Quaternion q1 = Quaternion.AngleAxis(90.0f, Vector3.right);
Quaternion q2 = Quaternion.AngleAxis(90.0f, Vector3.forward);
Quaternion delta = q2 * Quaternion.Inverse(q1);

float angle;
Vector3 axis;
delta.ToAngleAxis(out angle, out axis);

Debug.Log(Quaternion.Angle(q1, q2)); // 120
Debug.Log(angle);                    // 120

Lerp

Lerpは二つのQuaternionの間を線形補間(Linear intERPolation)する関数です。

Quaternionの補間は基本的に球面線形補間(Slerp)を使います。 しかし、球面線形補間は三角関数を何度か適用するため、膨大な回数繰り返す場合には少しだけ処理が重いです。 そこで、精度を犠牲にして高速化するための手段として線形補間と正規化による方法を使うことできます。

線形補間はQuaternion間の角度が0に近づくほど球面線形補間の結果に近づきます(過去記事参照)。 そのため角度が小さい場合にはそれほど違和感なく補間できますが、 角度が大きい場合には指定した影響度がうまく反映されずおかしな補間となります。

ちなみに、球面線形補間より速いといっても時間としては本当に微小なものです。 そもそも球面線形補間は体感できるほど遅い処理でもありませんので、 本当に膨大な回数実行する可能性がある場合にのみ使用することをおすすめします。

// 補間の角度が大きいと誤差も大きく
Quaternion a = Quaternion.AngleAxis(-90.0f, Vector3.up);
Quaternion b = Quaternion.AngleAxis(90.0f, Vector3.up);
Quaternion c = Quaternion.AngleAxis(45.0f, Vector3.up);
Quaternion lerp1 = Quaternion.Lerp(a, b, 0.75f);
Quaternion lerp2 = Quaternion.Lerp(b, c, 0.75f);
Quaternion slerp1 = Quaternion.Slerp(a, b, 0.75f);
Quaternion slerp2 = Quaternion.Slerp(b, c, 0.75f);

Debug.Log(lerp1 == c);  // False
Debug.Log(slerp1 == c); // True
Debug.Log(Quaternion.Angle(lerp1, slerp1)); // 8.130105
Debug.Log(Quaternion.Angle(lerp2, slerp2)); // 0.1119058

RotateTowards

RotateTowardsは二つのQuaternionの間を最大回転量を指定して補間する関数です。

Slerpでは補間に影響度を指定しましたが、この関数では最大回転量を指定します。 Slerpが中間的な回転を得る汎用性の高い関数である一方で、この関数は急激な変化を防ぐことに特化した関数です。 一度の変化に許容できる最大の回転量を指定することで急速な回転による不自然さを緩和させるために使います。 とはいえ、最大回転量のみによる制御というのはかなり簡易的なものなので、 挙動にこだわるのであれば角速度やアニメーションを導入した方がよいと思います。

最大回転量に正の値を指定すると近い方向から、負の値を指定すると遠い方向から回転します。 ただし不思議なことに、正の値では目標回転値で止まるのに対し、負の値では目標回転値に達してもなお回転を続けます。 直感的でないので負の値は指定しないことをおすすめします。

// 最大回転量で制限
Quaternion a = Quaternion.AngleAxis(-90.0f, Vector3.up);
Quaternion b = Quaternion.AngleAxis(90.0f, Vector3.up);
Quaternion c = Quaternion.AngleAxis(45.0f, Vector3.up);
Quaternion q1 = Quaternion.RotateTowards(a, b, 135.0f);
Quaternion q2 = Quaternion.RotateTowards(a, b, 720.0f);
Quaternion q3 = Quaternion.RotateTowards(a, b, -720.0f);

Debug.Log(q1 == c); // True
Debug.Log(q2 == b); // True
Debug.Log(q3 == a); // True

==演算子, !=演算子

==演算子は二つのQuaternionが一致するかどうかを判定します。 !=演算子は二つのQuaternionが一致しないかどうかを判定します。

==演算子と!=演算子は他のUnity構造体(ベクトル等)と同様、 完全には一致していなくても近い値であれば一致と判定してくれます。 そのため計算誤差があっても等値判定が可能です。

Quaternionにはその仕組み上、同じ回転を表す異なるQuaternionが二つずつ存在します(過去記事参照)。 しかし、==演算子や!=演算子はこれらを同じQuaternionとはみなさないため注意が必要です。 これらを同じとみなしたい場合にはDotを利用します。

// 同じ回転を表すQuaternionの等値判定
Quaternion q1 = Quaternion.AngleAxis(90.0f, Vector3.up);
Quaternion q2 = Quaternion.AngleAxis(90.0f + 360.0f, Vector3.up);
Quaternion q3 = Quaternion.AngleAxis(90.0f + 720.0f, Vector3.up);

Debug.Log(q1 == q2); // False
Debug.Log(q1 == q3); // True
Debug.Log(Mathf.Abs(Quaternion.Dot(q1, q2)) > 1.0f - Quaternion.kEpsilon); // True
Debug.Log(Mathf.Abs(Quaternion.Dot(q1, q3)) > 1.0f - Quaternion.kEpsilon); // True

重要度 ★★☆☆☆

eulerAngles

eulerAnglesはQuaternionのオイラー角表現を返すプロパティです。

オイラー角は人間にとってはわかりやすい形式ですが、プログラム的には扱いづらい形式です。 そのためオイラー角からQuaternionに変換することは多いですが、逆はそれほどありません。 このプロパティを使うのは主に回転値を編集するエディタ拡張を書く場合です。

オイラー角とQuaternionは一対一で対応しないことに注意してください。 オイラー角をQuaternionに変換し、再度オイラー角に戻したとしても、元のオイラー角の値とは一致しない可能性があります。

// 様々な要因で元とは異なる値が返る
Quaternion q1 = Quaternion.Euler(360.0f, 450.0f, 540.0f); // 360度以上
Quaternion q2 = Quaternion.Euler(-15.0f, -30.0f, -45.0f); // 負値
Quaternion q3 = Quaternion.Euler(120.0f, 240.0f, 360.0f); // Xが(90,270)の範囲
Quaternion q4 = Quaternion.Euler(90.0f, 60.0f, 30.0f);    // Xが90度または-90度

Debug.Log(q1.eulerAngles); // (0.0, 90.0, 180.0)
Debug.Log(q2.eulerAngles); // (345.0, 330.0, 315.0)
Debug.Log(q3.eulerAngles); // (60.0, 60.0, 180.0)
Debug.Log(q4.eulerAngles); // (90.0, 30.0, 0.0)

ToAngleAxis

ToAngleAxisはQuaternionの回転角度と回転軸を取得するメソッドです。

rotationプロパティのようにQuaternionが向きを表している場合には、 取得できる角度と軸は初期状態(Quaternion.identity)からの変化分であることに注意が必要です。 ある向きとの間の角度を取得したい場合にはAngle関数の利用を検討してください。

実はこのメソッドかなり出来が悪く、軸を反転させるにも関わらず角度が[0,360]の範囲で返ってきます。 どうもQuaternionがdouble-cover(過去記事参照)であることを考慮し忘れているみたいです。 360度回転のQuaternionに至っては軸の計算で0割りを起こし、InfinityNaNといった値が返ってきます。

// 回転値によっては期待した結果にならないことも
Quaternion q1 = Quaternion.AngleAxis(90.0f, Vector3.up);
Quaternion q2 = Quaternion.AngleAxis(450.0f, Vector3.up);
Quaternion q3 = Quaternion.AngleAxis(360.0f, Vector3.up);

float angle1, angle2, angle3;
Vector3 axis1, axis2, axis3;
q1.ToAngleAxis(out angle1, out axis1);
q2.ToAngleAxis(out angle2, out axis2);
q3.ToAngleAxis(out angle3, out axis3);

Debug.LogFormat("{0}: {1}", angle1, axis1); // 89.99999: (0.0, 1.0, 0.0)
Debug.LogFormat("{0}: {1}", angle2, axis2); // 270: (0.0, -1.0, 0.0)
Debug.LogFormat("{0}: {1}", angle3, axis3); // 360: (NaN, -Infinity, NaN)

Dot

DotはQuaternionの内積を計算する関数です。

Quaternionの理論についての理解が必要なため使う機会はそう多くないですが、内積の計算結果はいろいろと便利です。 例えば、角度比較の際にAngle関数の代わりに使って高速化したり、許容的な等値比較をする際にも利用できます。 また、独自の補間関数を定義したい場合にも必要になります。

LerpUnclamped, SlerpUnclamped

LerpUnclampedSlerpUnclampedはそれぞれ[0,1]に定義域を制限しないLerpSlerpです。

稀にですが、[0,1]でない範囲まで補間したい場合というのがあります。 例えば、ばねのような力によって一度目標値を通り過ぎてから戻るような挙動を実装したいときなどが考えられます。

重要度 ★☆☆☆☆

this[int], w, x, y, z

Quaternionを構成する値を直接変更したい場合にはインデクサや各要素名のフィールドが利用できます。

Quaternionの理論を理解した上で、独自の関数などを定義したい場合に使用します。

コンストラクタ

Quaternionには各要素の値を直接指定するコンストラクタが定義されています。

基本的にはQuaternionは各種関数を利用して作成するため、コンストラクタを直接呼び出す必要はありません。

SetXXX

Setから始まる各種メソッドは既存のQuaternionの値を新しい値で書き換えます。

Quaternionを新しい値で書き換えたい場合には代入演算子を利用するのが一般的です。 Setから始まるメソッドを使うと、読みづらい上に間違いやすいため封印推奨です。

参考

ユニティちゃんライセンス

ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

おわりに

もともと初心者向けにQuaternionの記事を書いていたのですが、 余計なことを書きすぎたせいでAPIの部分が難しくなったため分離しました。 次回こそ初心者向けの記事になる予定。

アニメGIFを作るために今回初めてTimelineを使ってみましたが、なかなか楽しいですねアレ。 まあゲーム製作において、使う場面はかなり限られそうですが・・・



執筆時のUnityのバージョン:2017.1.0f3

*1:ベクトルを列ベクトルとして扱うUnityの行列の仕様に合わせたものと思われる

*2:正確には三軸すべてで回さず、同じ軸で二度回してもいいため『各』座標軸ではないですが

*3:想定通りってなんだと言われると難しいのですが・・・

*4:日本語のスクリプトリファレンスをみると直交していると仮定すると書いてありますが嘘です

クォータニオンと回転

クォータニオン(四元数、Quaternion)は3Dグラフィックスのプログラミングにおいて回転を表す数としてよく出てきます。

曰く、

  • サイズが小さい(回転行列よりも少ない数で表せる)
  • ジンバルロックが起きない
  • 補間が容易

とのことで、非常に便利な理論です。

しかし、どういう原理で動いているのかを知りたいと思ってWikipediaなんかを見ると、大量の数式と謎の言葉の波に飲み込まれます。

そんなこんなで今までよくわからないままに使っていたのですが、 最近になって少しだけ理解が深まったので、現時点で知っていることについて説明してみようと思います。

対象読者

クォータニオンがどういうものなのか知りたい人向けです。 単にクォータニオンを利用した回転方法を知りたいだけであれば、他の資料を当たった方が簡単だと思います。

あまり難しい計算はしませんが、高校数学くらいの知識は持っているものとします。 具体的には、複素数、三角関数、ベクトル、行列についてです。

とはいえ、そんなによく理解していなくてもどうにかなるように書いたつもりです。 i^2=-1だとか、加法定理だとか、ベクトルと行列の計算方法だとか、回転行列がどんな形をしているかだとか、それくらいわかれば十分です。

複素数と座標

クォータニオンの話に入る前に準備として複素数の話をします。 というのも、クォータニオンというのは複素数の拡張だからです。

複素数というと確かx^2=-1のような二次方程式の解を表すために登場したなんだかよくわからない数でした。 ここではそのよくわからなさを理解しようとはせず、複素数の形だけに注目して2次元座標を表すことを考えます。

複素数は一般にx + iyという形で表せます。 このとき、xyは任意の実数で、両者はなんの関係性もない独立した数です。

そこで、xの値をx座標、yの値をy座標だと思い込むと、任意の複素数は2次元座標と一対一で結びつきます。

 \displaystyle
(x, y) \longleftrightarrow x + iy

2次元ベクトルが2次元座標を表すように、複素数もまた2次元座標を表すことができる2次元的な数なのです (ベクトルというと矢印や座標を思い浮かべるかもしれませんが、ベクトルの意味は単なる一並びの数であって、プログラミングにおける一次元配列に相当します)。

i^2=-1の関係はどこに行ったのかと思うかもしれませんが、 ベクトルに内積や外積といった特殊な掛け算が定義されているのと同じように、 複素数にも特殊な掛け算が定義されているというだけのことです。

複素数と回転

複素数が2次元座標を表せることはわかりました。 しかし、ベクトルではなく複素数を使う利点とは何でしょうか。 それは複素数の掛け算によって回転が簡単に表せることなのです (内積や外積により角度や直交方向が表せるのと同じ)。

ベクトルによる座標表現の場合、回転は回転行列を用いて次のように表せました。

 \displaystyle
\begin{pmatrix}
\cos \theta & -\sin \theta \\
\sin \theta & \cos \theta \\
\end{pmatrix}
\begin{pmatrix}
x \\ y \\
\end{pmatrix}
=
\begin{pmatrix}
x \cos \theta - y \sin \theta \\
x \sin \theta + y \cos \theta \\
\end{pmatrix}

これは座標(x, y)を、原点を中心に角度\theta回転させると、上式右辺のような座標に移るということを示しています。

さて、ここで次の複素数の計算式を見てください。

 \displaystyle
(\cos \theta + i \sin \theta)(x + iy) = (x \cos \theta - y \sin \theta) + i (x \sin \theta + y \cos \theta)

なんとなく先ほどの式に現れていたような部分が見て取れるのではないでしょうか。

左辺右側の複素数と右辺の複素数をそれぞれ座標だと考えると、左が回転前の座標、右が回転後の座標になっています。 回転行列の式と見比べると、\cos \theta + i \sin \thetaという複素数が回転行列と同じ役割を果たしていることがわかるかと思います。 つまり、複素数を用いると、\cos \theta + i \sin \thetaという複素数をかけるだけで角度\thetaの回転を表現できるのです。

天下り的な説明*1ではありますが、 複素数で回転を表せるということがわかりました。 ちなみに、どんな複素数でも回転を表すというわけではなく、絶対値(\sqrt{x^2 + y^2})が1の複素数だけが回転を表します。 これは回転を表す複素数 \cos \theta + i \sin \thetaの絶対値が1だからです(論理的には逆ですが)。

クォータニオンの定義

複素数は2次元的な数で、2次元の回転を表すことができました。 ということは、3次元版の複素数を考えれば3次元の回転ができるんじゃないか、というのは自然な発想ではないでしょうか。 しかし、実は複素数を3次元に拡張するのは満たすべき性質を考えると数学的に難しいことでした。 それならばと、ひとつ飛ばして4次元にまで拡張した複素数がクォータニオンです*2

クォータニオンは1つの実部と3つの虚部からなる数です。 3つの虚部が必要ということで、iに加えて新しい虚数単位jkを導入し、クォータニオンをw + ix + jy + kzのように表します。 wが実部で、x, y, zがそれぞれ虚部になります。

計算方法は概ね複素数と同じですが、乗算ではj^2ijといった項がでてきてしまうためi^2=-1のような演算規則が必要になります。 演算規則の定義は次の通りです。

 \displaystyle
i^2 = j^2 = k^2 = ijk = -1

ただし、クォータニオンでは行列の乗算のように右からかけるか左からかけるかで計算結果が異なります(交換法則が成立しない)。

上の定義に右から左から適当な数(i, j, k)をかけると、結局次のような関係が得られます。

 \displaystyle
\begin{array}{lll}
ij = k,  & jk = i,  & ki = j  \\
ji = -k, & kj = -i, & ik = -j \\
\end{array}

複素数が2次元座標を表現できたように、クォータニオンも4次元座標を表現できます。 例えば、座標(w, x, y, z)はクォータニオンw + ix + jy + kzと表せます。 4次元座標というとうまく想像できないかもしれませんが、計算上使うだけなので、単に4つ組の数字くらいに考えておくといいと思います。 重要なのは、どれか1つの数字を無視すれば、3次元座標になるということくらいです。

クォータニオンと回転

結論だけいうと、絶対値(\sqrt{w^2 + x^2 + y^2 + z^2})が1のクォータニオンは4次元の回転を表すことができます。

しかし、改めて考えてみると4次元の回転というのはいったいどんな操作なのでしょうか。

それを確かめるためにクォータニオンw + ix + jy + kzに対して、 回転を表すクォータニオン\cos \theta + i \sin \thetaを左からかけてみます (回転を表す複素数と同じ式ですが、ちゃんとy=0, z=0の回転を表すクォータニオンになっています)。

 \displaystyle
(\cos \theta + i \sin \theta)(w + ix + jy + kz) \\
\begin{split}
\quad
&= \cos \theta(w + ix + jy + kz) + \sin \theta(iw + i^{2}x + ijy + ikz) \\
&= \cos \theta(w + ix + jy + kz) + \sin \theta(iw - x + ky - jz) \\
&= (w \cos \theta - x \sin \theta) + i(w \sin \theta + x \cos \theta) \\
&\quad + j(y \cos \theta - z \sin \theta) + k(y \sin \theta + z \cos \theta)
\end{split}

これはつまり、次の座標変換を表しています。

 \displaystyle
\begin{pmatrix}
w \\ x \\ y \\ z \\
\end{pmatrix}
\overset{\cos \theta + i \sin \theta}{\longmapsto}
\begin{pmatrix}
w \cos \theta - x \sin \theta \\
w \sin \theta + x \cos \theta \\
y \cos \theta - z \sin \theta \\
y \sin \theta + z \cos \theta \\
\end{pmatrix}

ここで、実部とiの項、およびjの項とkの項についてそれぞれ見てみると、 それぞれが2次元における角度\thetaの回転式になっているのがわかります。 つまり、クォータニオン\cos \theta + i \sin \thetawx平面とyz平面をそれぞれ角度\thetaだけ回転させています。 4次元空間における回転といっても、結局は2次元平面上の回転なのです(3次元空間での回転でもそうですよね)。

また、この計算結果にはクォータニオンによる回転の重要な性質が現れています。 それは1点で交わる2つの平面*3上で同じ大きさの角度だけ回転させるというものです(isoclinic rotation)。 4次元空間における回転にもいくつか種類があるのですが、1つのクォータニオンの乗算で表現できる種類の回転はこれになります。

座標平面上の回転

先ほどは\cos \theta + i \sin \thetaを左からかけることにより、 wx平面とyz平面をそれぞれ角度\thetaだけ回転できることを確かめました。

同様にして、\cos \theta + j \sin \theta\cos \theta + k \sin \thetaを左からかけると、 wy平面とzx平面、wz平面とxy平面をそれぞれ角度\thetaだけ回転できることがわかります。

まとめると次のようになります。

  • \cos \theta + i \sin \theta左からかけるとwx平面とyz平面が角度\theta回転
  • \cos \theta + j \sin \theta左からかけるとwy平面とzx平面が角度\theta回転
  • \cos \theta + k \sin \theta左からかけるとwz平面とxy平面が角度\theta回転

乗算方向による回転の変化

クォータニオンの乗算では左右どちらからかけるかによって計算結果が異なるのでした。 回転操作は乗算なので、当然この影響を受けます。

変化を確かめるために\cos \theta + i \sin \thetaを今度は右からかけてみます。

 \displaystyle
(w + ix + jy + kz)(\cos \theta + i \sin \theta) \\
\begin{split}
\quad
&= \cos \theta(w + ix + jy + kz) + \sin \theta(iw + i^{2}x + jiy + kiz) \\
&= \cos \theta(w + ix + jy + kz) + \sin \theta(iw - x - ky + jz) \\
&= (w \cos \theta - x \sin \theta) + i(w \sin \theta + x \cos \theta) \\
&\quad + j(y \cos \theta + z \sin \theta) + k(-y \sin \theta + z \cos \theta) \\
&= (w \cos \theta - x \sin \theta) + i(w \sin \theta + x \cos \theta) \\
&\quad + j(y \cos (-\theta) - z \sin (-\theta)) + k(y \sin (-\theta) + z \cos (-\theta))
\end{split}

計算過程からわかるように、異なる二つの虚数単位の積が逆転することにより正負が反転し、 結果としてw軸を含まない平面の回転が逆向きになっていることがわかります。

他の座標平面に対する結果も合わせて次にまとめます。

  • \cos \theta + i \sin \theta右からかけるとwx平面が角度\theta回転、yz平面が角度(-\theta)回転
  • \cos \theta + j \sin \theta右からかけるとwy平面が角度\theta回転、zx平面が角度(-\theta)回転
  • \cos \theta + k \sin \theta右からかけるとwz平面が角度\theta回転、xy平面が角度(-\theta)回転

共役クォータニオンによる回転

あるクォータニオンに対して、虚部の符号を反転させたクォータニオンを共役クォータニオンといいます。 クォータニオンw + ix + jy + kzの共役クォータニオンはw - ix - jy - kzになります。 共役クォータニオンの表記法はいくつかありますが、ここではq^*のように星をつけて表記することにします。

定義より、\cos \theta + i \sin \thetaの共役クォータニオンは\cos \theta - i \sin \thetaです。 これを少し変形すると\cos (-\theta) + i \sin (-\theta)になるため、 この共役クォータニオンは逆向きの回転を表していることがわかります。 \cos \theta + j \sin \theta\cos \theta + k \sin \thetaの場合も同様です。

一般に回転を表すクォータニオンの共役クォータニオンは逆向きの回転を表します。 実部まで符号を反転させてしまうと逆回転にならないため注意してください *4

回転の合成

回転操作は乗算であり、クォータニオンの乗算には結合法則が成り立ちます。 よって、座標pをクォータニオンq_1で回転させた後にクォータニオンq_2で回転させたいという場合には、 予め計算しておいたクォータニオンq_{12} = q_2 q_1を用いて次のように計算できます。

 \displaystyle
q_2(q_1p) = (q_2q_1)p = q_{12}p

つまり、回転を表すクォータニオン同士の乗算により回転の合成ができるのです。

また共役クォータニオンにはq_1^*q_2^* = (q_2q_1)^*という性質が成り立つため、 共役クォータニオン同士の合成も元のクォータニオンの合成で表せます (共役クォータニオンは逆回転を表すので当たり前といえば当たり前ですが)。

 \displaystyle
q_1^*(q_2^*p) = (q_1^*q_2^*)p = (q_2q_1)^*p = q_{12}^*p

クォータニオンの乗算は左右どちらからかけるかによって結果が変わってしまうため、 合成の際には座標に対してどちらから回転を適用するかを考慮した上で、回転の順番を意識する必要があります。

クォータニオンと3次元回転

さて、ここまでは4次元の回転を考えてきましたが、我々が扱いたいのは3次元の回転です。 クォータニオンによる回転操作でどのように3次元の回転を実現するかを考えていきます。

3次元座標と4次元座標

クォータニオンによる回転操作は4次元空間にて適用するため、 3次元の座標は4次元の座標に変換する必要があります。

言葉にするとなんだか難しそうですが、やることは簡単でw成分を加えてやるだけです。 w成分の値はなんでも構いません。 というのも、理論上必要なだけで、3次元の回転操作ではこの値は一切関与しないためです。

仮にw値を0とすると、3次元座標(x, y, z)は次のようにクォータニオンで表せます。

 \displaystyle
(x, y, z) \longleftrightarrow (0, x, y, z) \longleftrightarrow 0 + ix + jy + kz

回転後の座標を表すクォータニオンも逆の手順で3次元座標へと変換できます。

回転の適用

前節では、各座標平面上の回転を表すクォータニオンを紹介しました。 これらはyz平面、zx平面、xy平面上で任意の角度の回転ができるものでした。 w成分を除いた3次元空間で考えると、これらはそれぞれx, y, z軸周りの回転を表します。 3つの座標軸周りの回転を合成すれば、3次元のあらゆる回転を表せそうです(参照:オイラー角)。

しかし、そうなると邪魔になってくるのが、同時に回転してしまうwx平面、wy平面、wz平面です。 どうにかして座標軸周りの回転のみを適用したいのですが、前節で述べたとおり、 クォータニオンの乗算では必ず二つの平面上で回転が起きてしまいます。

これを解消する方法は単純で、逆方向の回転によって打ち消してしまうことです。 具体的には次に示す演算をします。

 \displaystyle
qpq^*

pが変換前の座標、qが回転を表すクォータニオンです。 まず、クォータニオンを左からかけて通常通り回転させます。 同時に、共役クォータニオンを右からかけることでw軸を含む平面だけ逆回転させます。 右からかけるとw軸を含まない平面だけ逆回転しますが、 共役クォータニオンを利用することでそれがさらに反転し、w軸を含む平面だけが逆回転するようになります。 すると、w軸を含む平面は回転した量と同じだけ逆回転するため、回転が相殺されます。

一方、w軸を含まない平面は二倍回転することになるため、適用するクォータニオンの回転角度は半分にしておく必要があります。 角度\thetaの回転をさせたい場合には、\frac{\theta}{2}回転させるクォータニオンを乗算します。

各軸周りに対して、同じ操作をすると次のようになります。

 \displaystyle
q_{z}(q_{y}(q_{x}pq_{x}^*)q_{y}^*)q_{z}^* = (q_{z}q_{y}q_{x})p(q_{x}^*q_{y}^*q_{z}^*) = (q_{z}q_{y}q_{x})p(q_{z}q_{y}q_{x})^* = q_{xyz}pq_{xyz}^*

よって、各軸周りの回転を合成したクォータニオンq_{xyz}があれば、3次元座標にあらゆる回転(任意軸回転)を加えることができます。

さて、このq_{xyz}ですが、実はわざわざ合成しなくても次の式で簡単に計算できることが知られています。

 \displaystyle
\cos \frac{\theta}{2} + i r_x \sin \frac{\theta}{2} + j r_y \sin \frac{\theta}{2} + k r_z \sin \frac{\theta}{2}

\thetaは回転角度、rは回転軸の向きを表す単位ベクトル(大きさが1のベクトル)です。

計算手順のまとめ

  1. 3次元座標(x, y, z)をクォータニオンp = ix + jy + kzに変換
  2. rを回転軸として角度\theta回転させるクォータニオン q = \cos \frac{\theta}{2} + i r_x \sin \frac{\theta}{2} + j r_y \sin \frac{\theta}{2} + k r_z \sin \frac{\theta}{2}を計算
  3. 回転後の座標を表すクォータニオンp' = qpq^*を計算
  4. p' = ix' + jy' + kz'を3次元座標(x', y', z')に変換

クォータニオンと回転の一意性

4次元の回転において異なるクォータニオンが同一の回転を表すことはありません。 クォータニオンが表現できる種類の回転に対して、対応するクォータニオンは一意に定まります。

一方で、3次元の回転においては異なるクォータニオンが同一の回転を表すことがあります。 これは3次元回転の実現方法に起因しています。

3次元の回転ではひとつの平面における回転を打ち消すためにクォータニオンを二回乗算する必要がありました。 そもそもクォータニオンによる乗算は一回で[0, 2\pi)の角度の回転ができます。 これを二回乗算するのですから結果の回転角度は[0, 4\pi)になり、それぞれの角度に対応するものが二つずつ存在します(double-cover)。

回転を表すクォータニオンの計算式に\theta + 2\piを代入してみるとわかりやすいと思います。

 \displaystyle
\cos \frac{\theta + 2\pi}{2} + i r_x \sin \frac{\theta + 2\pi}{2} + j r_y \sin \frac{\theta + 2\pi}{2} + k r_z \sin \frac{\theta + 2\pi}{2} \\
\begin{split}
\quad
&= \cos (\frac{\theta}{2} + \pi) + i r_x \sin (\frac{\theta}{2} + \pi) + j r_y \sin (\frac{\theta}{2} + \pi) + k r_z \sin (\frac{\theta}{2} + \pi) \\
&= -(\cos \frac{\theta}{2} + i r_x \sin \frac{\theta}{2} + j r_y \sin \frac{\theta}{2} + k r_z \sin \frac{\theta}{2})
\end{split}

\thetaと同じ回転結果となるはずにも関わらず、クォータニオンは\thetaを代入した際の値の正負を反転させた結果となっています。

このように、3次元において同じ回転を表すクォータニオンは二つずつ存在するため、 クォータニオンが一致しないからといって異なる回転だと判断してはいけません。

クォータニオンの補間

クォータニオンを回転に利用する大きな動機のひとつは補間が容易であることです。

よく使われるクォータニオンの補間に球面線形補間(spherical linear interpolation, slerp)と線形補間(linear interpolation, lerp)があります。

球面線形補間

球面線形補間はクォータニオンにおける基本的な補間です。

回転を表すクォータニオンは絶対値が1であるのでした。 回転を表すクォータニオンを座標だと考えて4次元空間に配置すると、\sqrt{w^2 + x^2 + y^2 + z^2}が1なので、 これらのクォータニオンは必ず半径1の超球*5面上に存在します。 超球面上の二つのクォータニオンに対し、超球面上を通るように、最短距離を、一定速度で(線形に)移動するような補間を球面線形補間といいます。

4次元で考えづらい場合には、徐々に次元を上げていくとわかりやすいかもしれません。 2次元においては、円上の二点の間を、常に円上を通るように一定速度で移動するような補間です。 3次元になると球面上の二点の間を球面上を通るように移動する経路は無数にありますが、そのうちの最短の経路を一定速度で移動するような補間です。 4次元空間がどんな世界なのかは想像もつきませんが、同じような補間がきっとできるはずです。

球面線形補間を式にすると次の通り。

 \displaystyle
\frac{\sin(1-t)\theta}{\sin\theta}q_1 + \frac{\sin t\theta}{\sin\theta}q_2

\thetaは二つのクォータニオンの間の角度です。 4次元空間における角度ってどこだろう、と思うかもしれませんが、そもそも角度というのは内積によって定義されていて、

 \displaystyle
q_1 \cdot q_2 = |q_1||q_2|\cos\theta

を満たす\thetaこそが角度になります。 クォータニオンの内積は、クォータニオンを(w,x,y,z)という4次元ベクトルとみなした場合のベクトルの内積と同じです。

回転を表すクォータニオンでは絶対値が1なので、それらの内積の値は\cos\thetaに等しくなります。 よって、内積の値に逆三角関数(三角関数の逆関数で\sin\cosの値から角度を求める)を適用すれば角度\thetaが求まります。

線形補間

球面線形補間はきれいな補間ができるのですが、式をみればわかるようにそれほど単純な計算ではありません。 プログラミングにおいて、膨大な回数をこなすには少し計算量が多いのです。 そこで、近似的に少ない計算量で球面線形補間を実現する方法が線形補間です。

球面線形補間では超球面上を通るように補間しましたが、線形補間では直線経路で補間します。 直線経路で補間すると、補間結果のクォータニオンは基本的に超球面上にはありません。 つまり、回転をあらわすクォータニオンでなくなってしまいます。 これでは困るため、結果のクォータニオンをその絶対値で割ってやることで、絶対値が1になるように(半径1の超球面上に乗るように)正規化します。

線形補間を式にすると次の通り。

 \displaystyle
(1-t)q_1 + tq_2

この式は球面線形補間の式における、\thetaが非常に小さいときの近似式になっています (\thetaが十分に小さい場合に\sin\theta \simeq \thetaと近似できるため)。 大雑把な感覚として、補間しようとしている二つのクォータニオンが近いものであればあるほど、球面線形補間の値に近い値が得られます。

f:id:fspace:20170630220100p:plain f:id:fspace:20170630220111p:plain
2次元回転(複素数)の線形補間

補間における注意

クォータニオンの補間には一点だけ注意しておかなければならないことがあります。 それは、同じ回転を表すもうひとつのクォータニオンの存在です。

前節で述べたとおり、3次元の回転を表すクォータニオンは符号を反転させると、 まったく同じ回転を表すもうひとつのクォータニオンになります。 これらは3次元の回転としては同じでも、4次元空間における位置は原点を挟んでまったくの逆側です。 当然、どちらのクォータニオンとの間を補間するかによって結果はまったく異なります。

基本的に補間は距離の短い経路を用いた方が直感的になるため、 3次元の回転を表すクォータニオン同士を補間する場合には、符号をうまく反転させて近いクォータニオン同士を補間します。 どちらのクォータニオンの方が近いか判定するには角度を利用します。 二つのクォータニオンの内積を計算して、値が正なら|\theta| \lt \frac{\pi}{2}なのでそのまま補間を、 値が負なら|\theta| \gt \frac{\pi}{2}なので片方のクォータニオンの符号を反転させて補間をします。

参考

おわりに

クォータニオンに関する記事は、使い方のみに焦点を絞ったものや難しい数学の原理を説明したものが多く、 これら二つの間を埋めるような内容の記事が少ないな、と思ったので書いてみました。 できる限りわかりやすくする、という名目のもと難しい説明が必要な内容を(重要さに関わらず)片っ端からすっ飛ばしたので、 この記事を読んだだけでは疑問点も多いかもしれませんが、他の数学的記事の理解の助けになれば幸いです。

実はこの記事、うまい説明の流れが思いつかずに数か月間積んでます。 そのままお蔵入りにするのももったいないので、とりあえず書き残したことを加筆したのですが、 当初の構成を覚えていないのでバグ(論理の破綻)が紛れ込んでいる可能性が多分にあります。 何かおかしな点を見つけたら指摘してもらえると助かります。

*1:天の啓示のように突然式が出てきて「ほら正しいでしょ?」という類の説明。数学でよく使われる謎の言葉で人事云々とは全く関係ない。

*2:クォータニオンの発想の時点では特に回転のためではなく単純な数学的関心だったようです。

*3:想像しがたいかもしれませんが4次元空間なのでそういうことが起こります。

*4:演算子のカスタマイズが可能なプログラミング言語だと単純にマイナス符号をつけて逆回転とか思いがち(自分だけ?)ですが大抵そうはなりません。

*5:4次元以上の空間における円や球に相当するもの。

はてなブログテーマ "Notebook" のカスタマイズについて

Windows10のCreators UpdateにてEdgeがアップデートされ、 モダンブラウザのほとんどがCSS-Variablesという仕様に対応したため、 はてなブログテーマ“Notebook”のカスタマイズが容易になりました。

IEや古いAndroidブラウザ、Opera MiniなどはCSS-Variablesに対応していないため、 これらのブラウザで閲覧した場合にはカスタマイズ結果は反映されず、デフォルトの見た目で表示されます。

右の四角がに見えるなら対応ブラウザ、に見えるなら非対応ブラウザです。

カスタマイズ方法

Notebookテーマのインストール

当たり前ですが、まずはてなブログのテーマをNotebookにする必要があります。 テーマストアよりインストールしてください。 レスポンシブデザインなので、ブログメニューの「デザイン」>「スマートフォン」(スマホのアイコン)>「詳細設定」の「レスポンシブデザイン」にチェックを入れることをお忘れなく。

このカスタマイズ方法はNotebookテーマ専用です。 他のテーマで同様のカスタマイズをしたい場合にはテーマの製作者さんに問い合わせてみると、対応してくれるかもしれません。

デザインの変更

デザインの変更はブログメニューの「デザイン」>「カスタマイズ」(スパナのアイコン)>「デザインCSS」にて行います。

特別な設定(背景の指定など)をしていなければ、次のような記述になっているはずです。

/* <system section="theme" selected="10328749687191215643"> */
@import url("http://hatenablog.com/theme/10328749687191215643.css");
/* </system> */

この欄の一番下に次のように書き加え、{}の間に上書きしたい設定項目(後述)とその内容を書いていきます。

/* <system section="theme" selected="10328749687191215643"> */
@import url("http://hatenablog.com/theme/10328749687191215643.css");
/* </system> */

:root {
  (ここに設定項目を書いていく)
}

rootの前の:(コロン)を忘れないように注意してください。

設定項目は次の形式で一行ずつ書いていきます(CSSの書き方と同じ)。

(設定項目の名前):(設定値);
(設定項目の名前):(設定値);
(設定項目の名前):(設定値);

間に挟まっているのは:(コロン)で行末にあるのは;(セミコロン)です(どちらも半角)。 間違えないように注意。

設定を書き終えたら、「設定を保存する」を押して設定を反映すればOKです。

記述例

青系のデザインに変更する例。

:root {
    --color-main: #4040a7;
    --color-main-light: #644fcb;
    --color-main-dark: #3b3b79;
    --color-main-clip: #efecfa;
    
    --color-highlight-r: 100;
    --color-highlight-g: 78;
    --color-highlight-b: 203;
    
    --color-background: #ffffff;
    --color-border: #4040a7;
    
    --color-title: #4040a7;
    
    --border-width-entry: 1px;
    --border-width-sidebar: 1px;
}

f:id:fspace:20170410002840p:plain

設定項目一覧

すべての設定項目はハイフン二つ--から始まります。

色や大きさの指定方法はCSSの記述方法を参照してください。

「(未使用)」と書いてあるものは現在使っていませんが、 今後のデザイン変更で使用される可能性があります。 デザイン変更があった場合にデザイン崩れを最小限に抑えたい場合には設定しておくといいでしょう。

基本色

テキストカラー

設定項目 意味 デフォルト値
--color-text テキストの色 #454545
--color-text-light 明るめのテキストの色 #5f5f5f
--color-text-dark 暗めのテキストの色 #2c2c2c
--color-text-clip 切り抜き時のテキストの色(未使用) #c5c5c5
--color-text-white かなり明るめのテキストの色 #c5c5c5

メインカラー

設定項目 意味 デフォルト値
--color-main メインの色 #a74040
--color-main-light 明るめのメインの色 #cb644f
--color-main-dark 暗めのメインの色 #793b3b
--color-main-clip 切り抜き時のメインの色 #faefec

サブカラー

設定項目 意味 デフォルト値
--color-sub サブの色(未使用) #454545
--color-sub-light 明るめのサブの色 #5f5f5f
--color-sub-dark 暗めのサブの色(未使用) #2c2c2c
--color-sub-clip 切り抜き時のサブの色(未使用) #c5c5c5

ハイライト

設定項目 意味 デフォルト値
--color-highlight-r ハイライトの赤成分(0-255) 203
--color-highlight-g ハイライトの緑成分(0-255) 100
--color-highlight-b ハイライトの青成分(0-255) 78

要素色

背景

設定項目 意味 デフォルト値
--color-background 背景の色 #4c4c4c
--color-content コンテンツの背景の色 #ffffff
--color-border コンテンツの枠線の色 --color-textの値

タイトル等

設定項目 意味 デフォルト値
--color-title タイトルの文字色 #cccccc
--color-description ブログ説明文の文字色 --color-titleの値
--color-footer フッターの文字色 --color-descriptionの値

文章の一部

設定項目 意味 デフォルト値
--color-link リンクの文字色 --color-mainの値
--color-link-visited 訪問済みリンクの文字色 --color-main-lightの値
--color-link-hover マウスを重ねた際のリンクの文字色 --color-main-darkの値
--color-rule 罫線の色 --color-text-lightの値
--color-rule-light 明るめの罫線の色 --color-text-whiteの値

ソースコード

設定項目 意味 デフォルト値
--color-code-background コードの背景色 transparent
--color-code-statement 文キーワードの文字色 #3344ff
--color-code-type 型キーワードの文字色 #3344ff
--color-code-identifier 識別子の文字色 --color-textの値
--color-code-constant 定数の文字色 #cc4422
--color-code-preproc プリプロセッサの文字色 #3344ff
--color-code-comment コメントの文字色 #449933
--color-code-special 特殊キーワードの文字色 #cc4422

グローバルヘッダ

設定項目 意味 デフォルト値
--color-globalheader ヘッダの文字色 --color-main-clipの値
--color-globalheader-background ヘッダの背景色 --color-main-darkの値

色以外

各種大きさ

設定項目 意味 デフォルト値
--border-width-entry 記事の枠線の太さ 0
--border-width-sidebar サイドバーの枠線の太さ 0
--height-title-image タイトル画像の高さ 200px
--font-size-title タイトルの文字の大きさ 2rem
--font-size-description ブログ説明文の文字の大きさ 1rem

おわりに

Notebookテーマを使ってくれている方のブログをいくつか拝見したのですが、 カスタマイズしようと試みている人がけっこういるようだったので、 Edgeがアップデートされたタイミングでカスタマイズ方法を提供することにしました。

もともとオープンソースで改造可能ではあったのですが、SCSS必須ということで初心者にはちょっと難しい作業でした。 現在はCSS-Variablesのおかげで初心者でもなんとかできるレベルになったのではないかと思います。

明るめの色/暗めの色とか、赤成分/緑成分とか若干面倒な設定の仕方になっていますが、 CSSには色の計算や成分分解ができないという制限があるため現状ではどうしようもありません、ご容赦ください。 CSSはもっと柔軟な指定ができてもいいと思うのですが、パフォーマンスとかセキュリティを考えると案外難しいのでしょうね。