はじめに
この記事は、最近流行りのRust言語と、筆者が開発した四元数演算用ライブラリであるquaternion-coreを使って四元数の計算をしてみよう!という記事です。
「わかりにくい」「とっつきにくい」と言われることの多い四元数ですが、いきなり数式を見て悩む前に、まずは何かしら具体的な計算してみることで親しみが持てるのではないかと思います。 あと、折角なら私が開発したライブラリを使ってくれたら嬉しいな、というのもこの記事を執筆した理由の一つです。
本記事ではRustのインストール方法や言語仕様などには触れませんが、Rustについて詳しくなくても要点はわかるように書いたつもりです。
最低限cargo
コマンドが使用できる状態になっていれば大丈夫です。
quaternion-core について
quaternion-coreは筆者が大学生の頃に作り始めた四元数演算用ライブラリです。 四元数の基本的な演算から回転補間まで、三次元回転を扱う際に必要な機能を幅広く提供しています(ドキュメンテーションページはこちら)。
オイラー角、方向余弦行列、axis/angle、回転ベクトルといった各種回転表現との相互変換も可能で、特に24種類のオイラー角との相互変換ができるのは他の四元数演算ライブラリと比べてもかなり珍しいと思います。
また、quaternion-coreはマイコン上での使用を想定しており、no_std
環境での実行に対応しています。
これは単にno_std
環境でも同じように計算できるという話ではなく、fma
feature や norm-sqrt
feature を指定することでコンパイル時に計算方法を切り替え、計算量を削減することができます。
これは計算リソースの限られた環境において大きなメリットとなります。
元々は自分が使うために作り始めたライブラリですが、最近は予想以上に多くの方に使って頂けているようで、Rustの公式パッケージレジストリであるcrates.ioでは32,000ダウンロードに達しています(2024年10月17日時点)。
因みに、演算子オーバーロードを活用してより数式に近い書き方ができるquaternion-wrapperというライブラリも作りました。 話がややこしくなるので今回は使用しませんが、複雑な数式を手早く実装したいときは便利だと思います。
下準備
Rustで計算していくにあたり、まずは新しいプロジェクトファイルを作りましょう。 適当なディレクトリで以下のコマンドを実行して、sampleという名前のクレートを作ります(クレート名は何でもいいです)。
$ cargo new --bin sample
上記のコマンドを実行すると、以下のような構造のフォルダが作られます。
.
└── sample
├── Cargo.toml
└── src
└── main.rs
次に、quaternion-coreを使えるようにするためCargo.toml
ファイル内の[dependencies]
セクションに以下のように追記します。
[dependencies]
quaternion-core = "0.5"
あとはmain.rs
の先頭に
use quaternion_core as quat; // プログラム内では名前にハイフンを使えないので、アンダーバーに置き換えます。
と記述しておくことで、quat::add(a, b)
のようにしてquaternion-coreの機能を使用することができます。
ベクトルを回す
では早速具体的な計算を行っていきましょう。
あまり複雑にすると確認しにくくなるので、ここでは三次元ベクトル$( \boldsymbol{a} = [1 , 0 , 1] $)を$( \mathrm{Z} $)軸回りに$( \pi / 2 $)ラジアンだけ回して、回転後のベクトル$( \boldsymbol{b} $)を求めることにします。
※本記事では、見易さのためにベクトルを $( \boldsymbol{a} = [a_1, a_2, a_3] $) のように表記します。
この回転を図で表すと以下のようになります。この例では回転後のベクトルが$( \boldsymbol{b} = [0, 1, 1] $)となることは半ば自明ですが、任意の回転軸・回転角に対して考えられるのが四元数の強みです。
計算を行うために、まずは回転させるベクトル$( \boldsymbol{a} $)の変数を用意します。 quaternion-coreでは要素数3の配列をVector3型として扱っており、変数$( \boldsymbol{a} $)を定義するには以下のように書きます。
let a: quat::Vector3<f64> = [1.0, 0.0, 1.0]; // 各要素がf64(倍精度)のVector3型を定義
因みに、Rustには型推論があるので型名を省略して
let a = [1.0, 0.0, 1.0];
と書くことも出来ます。
ベクトルが用意できたので、次は$( \mathrm{Z} $)軸回りに$( \pi / 2 $)ラジアンの回転を表す四元数を作ります。 回転を表す四元数は、回転軸を $( \boldsymbol{n} (\| \boldsymbol{n} \| = 1) $) 、軸回りの回転角を$( \theta $)としたときに
$[ \begin{equation*} \tilde{q} = \cos \frac{\theta}{2} + \boldsymbol{n} \sin \frac{\theta}{2} \end{equation*} $]と書くことができます。今回は$( \boldsymbol{n} = [0, 0, 1] $)かつ$( \theta = \pi / 2 $)なので
$[ \begin{align*} \tilde{q} &= \cos \frac{\theta}{2} + \boldsymbol{n} \sin \frac{\theta}{2} \\ &= \cos \frac{\theta}{4} + [0, 0, 1] \sin \frac{\theta}{4} \\ &\approx (0.707, [0, 0, 0.707]) \end{align*} $]となります。quaternion-coreでは、from_axis_angle
関数を使うことで上記の四元数を作ることが出来ます。
let q: quat::Quaternion<f64> = quat::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0);
以上で、回されるベクトル$( \boldsymbol{a} $)と、所望の回転を表す四元数$( \tilde{q} $)を用意できました。 $( \tilde{q} $)が表す回転を$( \boldsymbol{a} $)に作用させ、回転後のベクトル$( \boldsymbol{b} $)を得るには、以下の式を用います。
$[ \begin{equation*} \boldsymbol{b} = \tilde{q} \boldsymbol{a} \tilde{q}^* \end{equation*} $]ここで、$( \tilde{q}^* $)は$( \tilde{q} $)の共役四元数を表します。
四元数の積はmul
関数で、共役四元数はconj
関数で計算できますので、上式を計算するには以下のようにします。
let tmp: quat::Quaternion<f64> = quat::mul(q, quat::mul((0.0, a), quat::conj(q))); // aは実部がゼロの四元数として扱う
let b: quat::Vector3<f64> = b.1; // タプルからベクトル部だけ取り出してbを定義.
四元数同士の積は四元数になるので、変数tmp
は四元数型になっています。
証明は省きますが、この計算では実部が必ずゼロとなるのでtmp
からベクトル部を取り出して最終的な計算結果b
を得ています(Quaternion型はタプルで実数型とVector3型をくっつけたものになっています)。
このように愚直に式を実装しても良いですが、ベクトルを回す式はよく使われるものなので、quaternion-coreではpoint_rotation
関数を使ってより簡潔に書くことが出来ます。
let b = quat::point_rotation(q, a);
見た目もシンプルですし、内部実装的にもゼロの掛け算を無くしている分だけ計算量が少ないのでpoint_rotation
関数を使ったほうが良いです。
ここまでの内容をまとめると、main.rs
の中身は以下のようになります。
use quaternion_core as quat;
const PI: f64 = std::f64::consts::PI; // 円周率
fn main() {
let a = [1.0, 0.0, 1.0]; // ベクトルaを定義
let q = quat::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0); // 回転を表す四元数を作る
let b = quat::point_rotation(q, a); // ベクトルを回す
println!("q: {:?}", q);
println!("a: {:?}", a);
println!("b: {:?}", b);
}
これを実行すると、次の計算結果が得られます。
$ cargo run
q: (0.7071067811865476, [0.0, 0.0, 0.7071067811865475])
a: [1.0, 0.0, 1.0]
b: [2.220446049250313e-16, 1.0, 1.0]
数値誤差の関係で若干のズレはあるものの、ほぼ$( \boldsymbol{b} = [0, 1, 1] $)となっています。 最初に示した図をもう一度確認してみてください。確かに正しい結果が得られていることがわかると思います。
余談ですが、直交座標系の模型(下図参照)を手に持ちながら考えるとかなり理解の手助けになります。
回転の合成
回転中心が同じである場合、複数の回転は1つの回転にまとめることが出来ます。
つまり、以下のように$( \boldsymbol{a} $)から$( \boldsymbol{b} $)、 $( \boldsymbol{b} $)から$( \boldsymbol{c} $)という2つの回転を行った場合、 2つの回転を合成して$( \boldsymbol{a} $)から$( \boldsymbol{c} $)への回転を表す四元数を作ることができます。
回転をまとめて何になるんだと思うかもしれませんが、例えば多関節アームロボットの手先姿勢(基準面に対する回転量)は各関節の回転角度を合成した結果になっているため、そういったところで役に立ちます。
細かいことはさておき、早速計算してみましょう。 四元数による回転の合成は非常に簡単で、四元数同士の積が回転の合成となっています。
つまり、1つめの回転を表す四元数を $( \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{b} } $) 、2つめの回転を表す四元数を $( \tilde{q}_{ \boldsymbol{b} \to \boldsymbol{c} } $) としたとき、これらを合成した四元数 $( \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{c} } $) は
$[ \begin{equation*} \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{c} } = \tilde{q}_{ \boldsymbol{b} \to \boldsymbol{c} } ~ \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{b} } \end{equation*} $]より計算でき、プログラムでは以下のように書きます(四元数の積は非可換なのでこの順序が重要です)。
let q_a2b = quat::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0); // 1つめの回転を表す四元数
let q_b2c = quat::from_axis_angle([0.0, 1.0, 0.0], PI / 2.0); // 2つめの回転を表す四元数
let q_a2c = quat::mul(q_b2c, q_a2b); // 合成結果
さて、このようにして合成した四元数はどのような回転軸・回転角になっているのでしょうか。quaternion-coreではto_axis_angle
関数を使うことで以下のようにして回転軸と回転角に変換できます。
let (axis, angle) = quat::to_axis_angle(q_a2c);
ここまでの内容をまとめると、main.rs
の中身は以下のようになります(先程のプログラムに付け足しても構いません)。
use quaternion_core as quat;
const PI: f64 = std::f64::consts::PI; // 円周率
fn main() {
let q_a2b = quat::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0); // 1つめの回転を表す四元数
let q_b2c = quat::from_axis_angle([0.0, 1.0, 0.0], PI / 2.0); // 2つめの回転を表す四元数
let q_a2c = quat::mul(q_b2c, q_a2b); // 合成結果
// 回転軸と回転角がどうなっているか見てみる
let (axis, angle) = quat::to_axis_angle(q_a2c);
// 一応ベクトルを回して確認
let a = [1.0, 0.0, 1.0];
let c = quat::point_rotation(q_a2c, a);
println!("axis: {:?}, angle: {}[deg]", axis, angle * (180.0 / PI));
println!("a: {:?}", a);
println!("c: {:?}", c);
}
これを実行すると、次の計算結果が得られます。
$ cargo run
axis: [0.5773502691896257, 0.5773502691896258, 0.5773502691896258], angle: 119.99999999999999[deg]
a: [1.0, 0.0, 1.0]
c: [1.0, 0.9999999999999999, 1.1102230246251565e-16]
to_axis_angle
関数はノルムが1の回転軸ベクトルを返すので0.577…という値をとっていますが、3軸全て同じ値なので回転軸は$( [1, 1, 1] $)と同じです。
また、軸回りの回転角は120度となっています。
$( \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{c} } $) が表す回転を回転軸と回転角の形でイメージするのは少し難しいですが、確認のためにベクトル$( \boldsymbol{a} $)を回した結果は$( \boldsymbol{c} = [1, 1, 0] $)となっており、確かに正しい回転を表していることが確認できます。
オイラー角に変換してみる
せっかくなので、前項で求めた $( \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{c} } $) をオイラー角に変換してみましょう。 回転軸と回転角に変換した場合よりは多少イメージしやすいかもしれません。
ところで、本記事の冒頭で「quaternion-coreは24通りのオイラー角との相互変換ができる」と述べたのを覚えているでしょうか。 オイラー角は回転順序によって12通りの表現が存在していることはご存知の方も多いと思いますが、更に
- 固定した座標系の各軸($( X, Y, Z $))を回転軸とする場合(Extrinsic rotation)
- 物体と共に回転する座標系の各軸($( x, y, z $))を回転軸とする場合(Intrinsic rotation)
という、回転軸をどの座標系でとるかという区別があるため、全部で24通りの表現が存在します。 一例として、最初にy軸回りに回転させて、次にz軸回りに回転させる場合を図にしてみました。
回転順序で違いが出るのはもちろんですが、どの座標系の軸回りに回転させるかによっても大きく違ってくることがわかると思います。
なので、回転表現を扱うライブラリにto_euler_angles
やfrom_euler_angles
のような名前の関数が存在したら、まずその関数で変換されるオイラー角が24種類あるうちのどれなのかを良く確認しなければなりません。
そもそも回転軸のとり方や回転順序に言及していないライブラリもあるので要注意です。
前置きが長くなってしまいましたが、ようやく $( \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{c} } $) をオイラー角に変換していきます。 全て同じ回転を表せるので24種類あるうちのどれに変換しても良いのですが、ここでは航空系でよく使われている(気がする)IntrinsicのZ-Y-X系オイラー角に変換することにします。 飛行機で言うとYaw-Pitch-Rollの順番で回転を表すものです。
quaternion-coreではto_euler_angles
関数を使うことで24種類全てのオイラー角に変換できます。
回転軸のとり方はRotationType
enumで指定し、回転順序はRotationSequence
enumで指定します。
今回はRotationType
とRotationSequence
の要素がそれぞれIntrinsic
とZYX
になるので、以下のように書きます。
let zyx_radian = quat::to_euler_angles(quat::RotationType::Intrinsic, quat::RotationSequence::ZYX, q_a2c);
to_euler_angles
関数ではオイラー角をVector3
型に入れて返します。
また、この時点では角度の単位がラジアンになっているので、ラジアンから度に変換するためにscale
関数を使って以下のようにします。
let zyx_degree = quat::scale(180.0 / PI, zyx_radian);
ここまでの内容をまとめると、main.rs
の中身は以下のようになります。
use quaternion_core as quat;
const PI: f64 = std::f64::consts::PI; // 円周率
fn main() {
let q_a2b = quat::from_axis_angle([0.0, 0.0, 1.0], PI / 2.0); // 1つめの回転を表す四元数
let q_b2c = quat::from_axis_angle([0.0, 1.0, 0.0], PI / 2.0); // 2つめの回転を表す四元数
let q_a2c = quat::mul(q_b2c, q_a2b); // 合成結果
// オイラー角に変換
let zyx_radian = quat::to_euler_angles(quat::RotationType::Intrinsic, quat::RotationSequence::ZYX, q_a2c);
let zyx_degree = quat::scale(180.0 / PI, zyx_radian); // [rad] ---> [deg]
println!("ZYX Euler angles: {:?}", zyx_degree);
}
これを実行すると、次の計算結果が得られます。
$ cargo run
ZYX Euler angles: [90.0, 1.2722218725854067e-14, 89.99999999999999]
計算結果を見ると、$( z $)軸周りに90度、$( y $)軸周りに0度、$( x $)軸周りに90度の順に 回転させれば、ベクトル$( \boldsymbol{a} $)から$( \boldsymbol{c} $)を得られることがわかります。 この回転を図にすると以下のようになります。
上図より、確かに $( \tilde{q}_{ \boldsymbol{a} \to \boldsymbol{c} } $) からオイラー角への変換が正しく行われていることが確認できました。
オイラー角が直感的に分かりやすいかと言われると「それはどうかな…?」と思ってしまいますが、 回転軸と回転角に比べればまあ頑張ったらイメージできそうという希望が持てる感じはありますね。
おわりに
どうでしょう、記事中で行った3つの計算例を通して少しは親しみが持てたでしょうか。 ここで示した計算例はどれも簡単なものでしたが、物体の回転を扱う上で基礎となる計算です。
本記事では四元数の数学的詳細にはほとんど触れなかったので、四元数について詳しく知りたい方は 以前執筆した『四元数まとめ資料』をご覧ください。 quaternion-coreで提供している演算はほぼ全て載っていますし、回転の合成を行う際になぜその順序で掛けるのかといったところもこの資料の中で触れています。