NZ旅行記
気分転換したかったので1週間ニュージーランドに行ってきました。
1日目〜2日目
フライトは成田→オークランドで約9時間、オークランド→クイーンズタウンで約2時間です。ニュージーランド航空を利用しました。機内食は美味しかったです。
ニュージーランドでは自然環境保護のために、植物や肉類の持ち込みが厳しく制限されています。持ち込み物の申告漏れの場合は400ドルの罰金が科せられます。税関の列待ちは1時間くらいかかりました。動食物検察探知犬がいて少し緊張感がありました。
国内線に乗り継いでオークランドからクイーンズタウンまで移動します。空港で乗り継ぎ用のタグを引きちぎってしまい慌てて再発行してもらいました。国際線→国内線はタグを変える必要が無いのか…クイーンズタウンへの国内線はあまり人が乗っていなかったため快適でした。機内ではproper chips(ビネガー&塩味)というポテトチップスが配られましたが、とても酸っぱかったです。
クイーンズタウンに着きました。自然に囲まれた小さい町です。
空港からクイーンズタウン市内への移動手段はいろいろありますが、1番安いのはorbusと呼ばれる市バスでの移動です。bee cardという交通カードをバスの運転手から購入すると、通常料金1ドルになります。空港からだと5ドルになります。コロナの影響でバスの本数が減っている上に料金システムが分かりにくいため困惑しました。割と適当なので、2人で1枚のカードを使えたり、降車の際にカードをかざさなくても自動で1ドル支払われるようです。(降りるときにtag offしろと書いてはありますが。)一番困ったのがバスが30分に1本ということです。自分は地方に住んでいるのでこの状況には慣れていましたが、旅先ではかなり不便に感じました。
到着日はクイーンズタウンマラソンとかいうローカルイベントがあるせいで、1か月前には宿がほとんど埋まっていました。計画性が無いことを久々に反省しました…また、クイーンズタウン中心部は土曜日のためか飲んでる人が多かったです。夜はクラブ音楽が流れていましたが、適度な飲み方を知っている大人が多く治安は良かったです。お酒に合うご飯頼んでしまいましたが、翌日は朝が早いので飲むのは我慢しました。
あとは有名なパタゴニアチョコレートのアイスクリームを食べました。コーンがチョコレートでコーティングされていて美味い。アイスの種類は無難にクッキー&クリームにしました。中にオレオが入っていて嬉しい。
ここで気づき始めたのが自販機がどこにもないことです。仕方ないのでスーパーでペットボトルの水2Lを3ドルで購入しました。ちょっと高い。みんな飲み物はどうしているんだ…数日後に判明しましたが、ニュージーランドの水は弱アルカリ性で日本と同様、水道水を飲んでも問題ないようです。また、プラスチックを削減する政策が進んでいて、なるべくペットボトルは買わない傾向があるようです。500mlだと大体左写真のようなペットボトルしか見かけませんでした。
高緯度地域で夏至が近いため、21半時くらいまで明るく不思議な感じでした。
3日目
クイーンズタウンからテカポまで移動します。間256kmと遠く、バスで片道4時間かかります。山道が多いので車酔いしやすいです(酔い止め必須)。コロナの影響でgreatsights社のテカポまでのバスは無かったので、intercity社のバスを利用しました。道中には羊がたくさんいます。羊が多すぎる。毛刈りされた羊は痩せ細っていて、一見大人と子供の違いが判りません。
テカポに着いたときは雨が降ってました。天気が悪いと星が見れないので困ります。宿にも移動できないので、仕方なく雨が病むまでカフェで待つことにしました。とはいってもお店が少ないので選択肢がありません。何とも言えない味のスープが出てきました。トマトベースのスープにアジア料理系のスパイスが入ったような味がしました。癖が強いし量も多くてつらかった。
宿に移動します。とても良い宿でした!2019年にできたロッジみたいですが、ベッドルームが2部屋あって広いリビングに暖炉もありました。キッチンには大きい冷蔵庫や電子レンジが備わっており、調理器具もカトラリーだけでなく泡だて器があるくらい揃っています。長期滞在だと余裕をもって自炊したりできるのでより楽しめそうです。バスタブも清潔に保たれてました。手作りのパンや紅茶、ココアもいただけて快適すぎました。中心部からは徒歩15分くらいで大したことないかと思っていたのですが、荷物が多かったため疲れて3時間ほど仮眠しました。起きたら雨が止んでいて良かったです。
夜ご飯を食べにいきます。ラム肉のソテーとサーモンのマリネをいただきました。いただいたカクテルはテカポの湖をモチーフにしたものです。普通に美味しかったです。20時くらいになってもまだ明るいのでテカポの観光スポット、羊飼いの教会を訪れたところ、雨上がりの虹が出ていました!ルピナスの花も咲いていてすごく綺麗です。

日が沈んだ後には星空観測のツアーに参加しました。自分は申し込みが遅くてdark sky projectのマウントジョン展望台ツアーには参加できなかったので、違うツアーに申し込みました。2か月くらい前に予約しておきましょう…少人数(3組)のツアーに参加しましたが、丁寧に説明してくれたことと、素敵な写真を撮っていただけたのでとても満足しています。途中で雲が無くなり、人工衛星や流れ星、南半球の星空をみることができたので良い思い出になりました。
4日目
朝は動物に餌をあげたりしました。とっても可愛い。子羊にはモーテルの人が毎日ミルクをあげているようです。
昼は数少ないお土産屋さんを見たりしてからテカポを経ちました。4時間かけてクイーンズタウンまで戻ります。爆睡して16時にクイーンズタウンのfrankton hubに降りました。コロナの影響でバスの本数が少ないので、仕方なくホテルまでタクシーで移動しました。夜はホテル近くのスーパーで買い出しして気になるものを食べました。酸っぱい食べ物が多かったです。
5日目
ミルフォードサウンドのツアーに参加します。片道287kmをバスで4時間です。朝は晴天でしたが、雨の予報だったので諦めムードでバスに乗りました。バスの運転があまりにも速い上に山道なので酔います。酔い止めは持っていきましょう。自分は耐え切れず途中の町で追加の酔い止めを購入しました。道中で羊が道路を塞いでいてバスが通れないという可愛いイベントに遭遇し、酔いも和らいだ頃、天気が急変しました。ミルフォードサウンド付近は半分くらい雨になるようなので仕方ないです。

途中で野生のkeaに会えました。ミヤマオウムとも呼ばれ、ニュージーランドの固有種です。羽の内側が鮮やかなオレンジ色でとても綺麗です。keaはとても知能の発達していて、いたずら好きな鳥としても知られています。人間が近づいても逃げません。翌日に訪れたkiwi parkでは10cmくらいの距離まで近づくことができました。

ミルフォードサウンドに着いてからは雨が止んだり降ったりするようになりました。山が水面に反射している有名な写真と同じ景色を見ることはできませんでしたが、山頂は隠れていなかったので良かったです。写真ではあまり伝わりませんが、スケールが大きく、滝の威力は凄まじかったです。また、ミルフォードサウンドはフィヨルド地形であり、海に近づくほど波が激しく船も大きく揺れました。

帰りのバスも酔いに苦しみながらなんとかホテルに戻りました。ホテル周辺の景色も素晴らしく、どこで写真を撮っても自然を楽しめる国だと思います。街中でもごみは一切落ちておらず、生き物に餌を与えたりするマナーの悪い人を見かけませんでした。自分のような観光客にも話しかけてくれる人が多く、(運が良かっただけかもしれませんが)差別的な態度を取られることは一度もありませんでした。
6日目
市内観光をしました。また、ゴンドラに乗って山頂に行くとクイーンズタウンの街全体が一望できます。この山では冬にはスキーができるようです。今は春のため雪は積もっていませんでしたが、途中でシカのような生き物が草を食べているのを目撃しました。結構高いところまで登ってくるんだなと思いました。羊や牛とは生息域が重ならないようになっているのかもしれません。雨が降り続いていたので撮影のために止むのを待ちました。日本では雨が降ると傘をさす文化がありますが、他の国ではあまり傘をさしません。傘をさしていると観光客と思われるので途中からさすのをやめました。雨上がりの景色は綺麗でした。
ゴンドラでたどり着ける山頂でご飯が食べれるのですが、結構おいしいです。自分はミートパイとクリームブリュレをいただきました。地味に量が多かった。ここでは街を一望しながらビュッフェが食べられるのですが、あいにくこの日はビュッフェが休みでした。クイーンズタウンでは全体的に、コロナの影響かもしれませんが、休業中であったり早めに閉まってしまうお店が多かったです。
kiwi parkに行きました。ゴンドラの出発点付近に施設があるのでアクセスがよいです。kiwiの写真は撮れません。彼らは夜行性で音にも敏感なため、暗い室内でのみ展示されています。入場料は4000円と高いのですが、これはkiwi parkが国や自治体ではなく自発的にkiwiの保護活動をする人々によって運営されているからです。飼育されているkiwiは成長したら野生に帰ります。またニュージーランドのお土産にPossumと羊の毛で作られたニットや靴下、マフラー(Possum merino)がよく売られていますが、これはPossumがkiwiのエサを食べたりしてニュージーランドの生態系に害をもたらす害獣とされているからです。
夜はラム肉料理をテイクアウトしました。極寒の中外で食べるしかなかったことと、量が多くて苦しみました。味は大変素晴らしく、ラム肉特有の臭みもほとんど気になりませんでした。
7日目
市内で買い忘れたお土産を買ったりしました。パタゴニアのアイスクリーム(ストロベリー味)を再び食べました。街沿いに面しているワカティプ湖の透明度に驚きました。鳥たちも優雅に泳いでいました。空気も澄んでいてとてもリラックスできます。夜にクイーンズタウンからオークランドへ移動しました。
8日目
帰国しました。オークランドから日本への飛行機はなぜかオンラインチェックインできず、焦りました。おそらくワクチン接種証明書を見せる必要があるためかと思われます。11月から導入されたvisit japan webのQRコードを見せたのですが、拒否され焦りました。結局ワクチン接種の紙3枚を見せたところ許可されたのですが、何が正解だったのかよく分かっていません。日本はいつになったら入国緩和されるんだ…入国を厳しくするのであればもう少し手続きを分かりやすく統一して欲しいものです。成田空港でも列を誘導する人がたくさんいて、人件費が気になりました。
回転の表現について
はじめに
筆者は数学を勉強しているわけではないので、間違いがあれば指摘してください。ちゃんと勉強したい人はSEGAさんの基礎線形代数講座を読むことをお勧めします。
3dcgで回転を表現する方法として、回転行列、オイラー角、クォータニオンが主に挙げられると思います。それぞれの特徴をまとめました。
回転行列
3×3行列の直交行列で回転を表し、9つのパラメータを使います。直行行列である拘束条件(\(\mathrm{R R^T}=\mathrm{R^T R}=\mathrm{E}\))により、自由度は3になります。また、任意軸周りの回転を表すロドリゲスの回転行列に対応させることができ、以下のように\(n_x, n_y, n_z, θ\) の4つのパラメータで回転を表せます。回転軸nのノルムが1であるという拘束条件により、自由度が3つになることが分かります。
ロドリゲスの回転公式
\(
R_n(θ)= \begin{pmatrix}
\cosθ+n_x^2(1-\cosθ) & n_x n_y(1-\cosθ)-n_z\sinθ & n_z n_x(1-\cosθ)+n_y\sinθ \\
n_x n_y(1-\cosθ)+n_z\sinθ & \cosθ+n_y^2(1-\cosθ) & n_y n_z(1-\cosθ)-n_x\sinθ \\
n_z n_x(1-\cosθ)-n_y\sinθ & n_y n_z(1-\cosθ)-n_x\sinθ & \cosθ+n_z^2(1-\cosθ)
\end{pmatrix}
\)
オイラー角
※今回は右手系、内因性(intrinsic)の場合を考えます。

オイラー角とは
オイラー角では、3つの角度の組み合わせで回転を表現します。 剛体に固定された座標系で、「X軸周りにα、Y軸周りにβ、Z軸周りにγ回転させる」といった感じで表せ、直感的に分かりやすいです。
型が多い
まず、オイラー角の回転には親子関係があります。具体的にXYZ順の場合、Y軸を回転すればX軸が回転し、Z軸を回転すればY軸が回転し、それに起因してX軸も回転します。こちらにXYZ順の回転を実装したものを置いておきました。
親子関係によって軸が回転するということは、XYZの順とYXZの順では結果が変わってきます。この時点で6通りの表し方があります。また、最初の回転と最後の回転で同じ座標軸を使うことができます。1番目と2番目で同じ座標軸を使っても2番目は冗長になるので、1つ飛ばして同じ回転軸を使用するということです。1番目と3番目で同じ軸を使うものは狭義のオイラー角といい、違う軸を使う場合はタイトブライアン角とも言います。結局これらを考慮すると、以下のように12通りの回転の表し方が存在することなります。
| ABC順 | XYZ | XZY | YXZ | YZX | ZXY | ZYX |
| ABA順 | XYX | XZX | YXY | YZY | ZXZ | ZYZ |
※UnityではZXY順、blenderでは選択できるようになっています。
blenderでの回転順の選択箇所

ジンバルロック
XYZ順の場合、2番目の軸を±π/2 回転させた時にX軸とZ軸が平行になり、自由度が1つ失われるという現象が起きます。XYX順の場合は2番目の軸を0またはπ回転させた時に同様の現象が起きます。これをジンバルロックと言います。こちらのXYZ順でもβの値を90度か270度にしたとき、αとγによる回転がどちらもz軸周りの回転になることが確認できます。3軸の回転を表現したかったのに、2軸の回転に成り下がってしまうというのは計算上かなり問題になってきます。実際、アポロ計画ではあらかじめジンバルロックにならないようなコースを選び、手動で調節していたらしいです。
※航空学ではピッチ角(β)が90度になる可能性が非常に低いので今でもよく用いられるようです。
XYZ順の回転で、ジンバルロックが起きたときの回転行列を考えてみます。β=π/2 であるとき、
\begin{eqnarray}
R&=& \begin{pmatrix}
\cosγ & -\sinγ & 0 \\
\sinγ & \cosγ & 0 \\
0 & 0 & 1\end{pmatrix}\begin{pmatrix}
0 & 0 & 1 \\
0 & 1 & 0 \\
-1 & 0 & 0
\end{pmatrix}\begin{pmatrix}
1 & 0 & 0 \\
0 & \cosα & -\sinα \\
0 & \sinα & \cosα
\end{pmatrix}\\[15px]&=& \begin{pmatrix}
0 & -\sinγ & \cosγ \\
0 & \cosγ & \sinγ \\
-1 & 0 & 0
\end{pmatrix}\begin{pmatrix}
1 & 0 & 0 \\
0 & \cosα & -\sinα \\
0 & \sinα & \cosα
\end{pmatrix}\\[15px]&=& \begin{pmatrix}
0 & \sin(α-γ) & \cos(α-γ) \\
0 & \cos(α-γ) & -\sin(α-γ) \\
-1 & 0 & 0
\end{pmatrix}
\end{eqnarray}
上記より、αとγの変化が回転行列Rに与える影響は同じであることが分かります。γの符号が負なので、αとγによる回転の向きは反対になります。x軸の回転はz軸周りの回転と等しくなることが分かっているので、αによる回転は余分になります。
回転行列からオイラー角
せっかくなので回転行列からオイラー角(XYZ順)を求めてみましょう。
\begin{eqnarray}
R&=& \begin{pmatrix}
\cosγ & -\sinγ & 0 \\
\sinγ & \cosγ & 0 \\
0 & 0 & 1
\end{pmatrix}\begin{pmatrix}
\cosβ & 0 & \sinβ \\
0 & 1 & 0 \\
-\sinβ & 0 & \cosβ
\end{pmatrix}\begin{pmatrix}
1 & 0 & 0 \\
0 & \cosα & -\sinα \\
0 & \sinα & \cosα
\end{pmatrix}\\[15px]&=& \begin{pmatrix}
\cosβ\cosγ & \sinα\sinβ\sinγ-\cosα\cosγ & \cosα\sinβ\cosγ+\sinα\sinγ\\
\cosβ\sinγ & \sinα\sinβ\sinγ+\cosα\cosγ & \cosα\sinβ\sinγ-\sinα\cosγ \\
-\sinβ & \sinα\cosβ & \cosα\cosβ
\end{pmatrix}
\end{eqnarray}
よって、
\(
m_{20}=-\sinβ
\)
\(\displaystyle β= \mathrm{arc} \sin (-m_{20}) \)
また、β≠±π/2のとき、
\(
m_{21}= \sinα\cosβ, m_{22}= \cosα\cosβ
\)
\(\displaystyle \sin α=\frac{m_{21}}{\cosβ}\)
\(\displaystyle \cos α = \frac{m_{22}}{\cosβ}\)
\(\displaystyle \tan α=\frac{\sinα}{\cosα} = \frac{m_{21}}{m_{22}}\)
\( \displaystyle α= \mathrm{arc} \tan ( \frac{m_{21}}{m_{22}} ) \)
同様に、
\(m_{10}= \cosβ\sinγ, m_{00}= \cosβ\cosγ\)
\(\displaystyle \sin γ=\frac{m_{10}}{\cosβ}\)
\(\displaystyle \cos γ = \frac{m_{00}}{\cosβ}\)
\(\displaystyle \tan γ=\frac{\sinγ}{\cosγ} = \frac{m_{10}}{m_{00}}\)
\( \displaystyle γ= \mathrm{arc} \tan ( \frac{m_{10}}{m_{00}} ) \)
まとめ
β≠±π/2のとき、
\(\text{}
\begin{cases}
\displaystyle α= \mathrm{arc} \tan ( \frac{m_{21}}{m_{22}} ) \\
\displaystyle β= \mathrm{arc} \sin (-m_{20}) \\
\displaystyle γ= \mathrm{arc} \tan ( \frac{m_{10}}{m_{00}} )
\end{cases}\)
β=±π/2のとき、
\(\text{}
\begin{cases}
\displaystyle α= 0 \\
\displaystyle γ= \mathrm{arc} \tan ( -\frac{m_{01}}{m_{11}} )
\end{cases}\)
クォータニオン
なぜクォータニオンが使われるのか
先ほど取り上げた回転行列ではパラメータが9つあり、余分にメモリが取られます。またオイラー角には、ジンバルロックが存在します。こういった問題に遭遇しない、ちょうど良い回転表現がクォータニオンになります。
クォータニオンとは
クォータニオンは四元数とも呼ばれます。複素数は虚数成分iを用いて\(x+iy\)と表すことができますが、これに虚数成分jとkを加えたものがクォータニオンになります。複素数は2次元の回転を扱うことができるので、それを応用して3次元の回転を表現しようとしたわけです。本来なら虚数成分はiとjの2つで済ませたいところですが、それでは上手くいかなかったためkを追加しました。それで四元数となっています。
クォータニオンの導入
2次元の複素数平面では1-i平面の1平面のみで回転させていました。クォータニオンは4次元なので、6平面で回転させる必要があります。
| 平面 | 乗算 | 計算結果 | 条件 |
| 1-i | ×i | 1→i→-1→-i→1 | |
| 1-j | ×j | 1→j→-1→-j→1 | |
| 1-k | ×k | 1→k→-1→-k→1 | |
| j-k | ×i | j→k→-j→-k→j | ij=k, ik=-j |
| k-i | ×j | k→i→-k→-i→k | jk=i, ji=-k |
| i-j | ×k | i→j→-i→-j→i | ki=j, kj=-i |
※i→j→-i→-j→iとなるのが理想でしたが、j→ij→-j→-ij→jとなって上手く行かなかったので、kを導入して4次元にすることで辻褄を合わせたという感じです。
しかし、iをかける(\(\frac{π}{2}\) 回転させる)と、1-i平面とj-k平面で回転してしまうことが分かります。同様に、jをかけると1-j平面とk-i平面で回転し、kをかけると1-k平面とi-j平面で回転してしまいます。2つの平面で回転するのは困るので、何とかして1つの平面だけを回転させたいです。ここで、右から-iをかけたときを考えてみます。
| 平面 | 乗算 | 計算結果 |
| 1-i | ×(-i) | 1→-i→-1→i→1 |
| j-k | ×(-i) | j→k→-j→-k→j |
左からiをかけたときと比較して、1-i平面では反対向き、j-k平面では同じ向きに回っていることが分かります。このことから、回転を表現するクォータニオンを、\( q= a\cos \frac{θ}{2}+i b\sin \frac{θ}{2}+ j c\sin \frac{θ}{2}+k d\sin \frac{θ}{2}\)と定義し、左からクォータニオン、右から共役クォータニオンを乗算することで回転を表現するようにしました。\(\frac{θ}{2}\)となっているのは、左右からクォータニオンを乗算したときに回したい角度の2倍回転してしまうからです。点p=(x,y,z)を回転させたものをp'と置いたとき、
\begin{equation}\boldsymbol{p'= qp\bar{q}} \tag{1}\end{equation}
p'のi, j, k成分が回転後の座標になっています。
回転行列による表現
(1)式に対して、\(q=q_{0}+ \boldsymbol{q}\)と置くと(\(q_{0}\)はスカラー部)
\begin{eqnarray}
\boldsymbol{p'} &=& q\boldsymbol{p}\bar{q} \\
&=& (q_{0}+\boldsymbol{q})\boldsymbol{p}(q_{0}-\boldsymbol{q}) \\
&=& q_{0}^2 \boldsymbol{p} + q_{0}\boldsymbol{(qp-pq)} -\boldsymbol{qpq} \\&=& q_{0}^2 \boldsymbol{p} + 2q_{0}(\boldsymbol{q×p})-\{-\boldsymbol{(p・q)q+(q・q)p-(q・p)q-q・(p×q)} \tag{2}\}\\&=& (q_{0}^2-\boldsymbol{q・q}) \boldsymbol{p} + 2\boldsymbol{(q・p)q}+2q_{0}(\boldsymbol{q×p}) \tag{3}
\end{eqnarray}
※(2)式は\(\boldsymbol{pq}=-\boldsymbol{p・q}+\boldsymbol{p×q}\)、(3)式はq同士の外積が0になることを利用
\( \boldsymbol{p'}=\begin{pmatrix}p_{1}' \\ p_{2}' \\ p_{3}' \end{pmatrix}\) \(\boldsymbol{p}=\begin{pmatrix} p_{1} \\ p_{2} \\ p_{3} \end{pmatrix}\) \(\boldsymbol{q}=\begin{pmatrix} q_{1} \\ q_{2} \\ q_{3} \end{pmatrix}\)と置くと、以下のように計算できます。
\begin{eqnarray}
p_{i}'&=&(q_{0}^2-\boldsymbol{q・q}) p_{i}+ 2\sum_{j=1}^{3} q_{j}p_{j}q_{i}+2q_{0}\sum_{k,j=1}^{3}ε_{ikj}q_{k}p_{j} \\ &=& \sum_{j=1}^{3} \{ (q_{0}^2-\boldsymbol{q・q}) δ_{ij}+2q_{i}q_{j}-2q_{0}\sum_{k=1}^{3}ε_{ijk}q_{k} \}p_{j}\end{eqnarray}
\begin{eqnarray}R_{ij}=(q_{0}^2-\boldsymbol{q・q})δ_{ij}+2q_{i}q_{j}-2q_{0}\sum_{k=1}^{3}ε_{ijk}q_{k}\end{eqnarray}
※\(ε_{ijk}\)は3階のエディントンのイプシロン
ここから、以下の行列式が求まります。
\begin{eqnarray}\begin{pmatrix}p_{1}' \\p_{2}' \\p_{3}' \end{pmatrix}=\begin{pmatrix}
q_{0}^2+q_{1}^2-q_{2}^2-q_{3}^2&2(q_{1}q_{2}-q_{0}q_{3})&2(q_{1}q_{3}+q_{0}q_{2}) \\
2(q_{2}q_{1}+q_{0}q_{3})&q_{0}^2-q_{1}^2+q_{2}^2-q_{3}^2&2(q_{2}q_{3}-q_{0}q_{1}) \\
2(q_{3}q_{1}-q_{0}q_{2})&2(q_{3}q_{2}+q_{0}q_{1})&q_{0}^2-q_{1}^2-q_{2}^2+q_{3}^2
\end{pmatrix}\begin{pmatrix}p_{1} \\p_{2} \\p_{3} \end{pmatrix}\end{eqnarray}
これでクォータニオンから回転行列への変換ができました。
ロドリゲスの回転公式
(3)式で、\(q_{0}=\cos \frac{θ}{2}、\boldsymbol{q}=\sin \frac{θ}{2}\boldsymbol{u}\)と置くと、
\begin{eqnarray}
\boldsymbol{p'} &=& (q_{0}^2-\boldsymbol{q・q}) \boldsymbol{p} + 2\boldsymbol{(q・p)q}+2q_{0}(\boldsymbol{q×p})\\
&=& (\cos^2 \frac{θ}{2}-\sin^2 \frac{θ}{2}) \boldsymbol{p} + 2\sin^2 \frac{θ}{2}\boldsymbol{(u・p)u}+2\cos \frac{θ}{2} \sin \frac{θ}{2}(\boldsymbol{u×p})\\&=& \boldsymbol{p}\cosθ +(1-\cos θ)\boldsymbol{(p・u)u}+(\boldsymbol{u×p})\sin θ
\end{eqnarray}
この式はロドリゲスの回転公式のベクトル表記になります。このことから、回転行列とクォータニオンは本質的には同じものであることが分かります。
球面線形補間
クォータニオンのメリットとしてよく挙げられるのが球面線形補間(slerp)ができることです。考え方についてはCGのための数学のクォータニオンの部分で分かりやすく説明されています。2つのクォータニオン\(q_{1}、q_{2}\)に対して、補間されたクォータニオン\(q_{t}\)が以下のように表現されます。
\begin{eqnarray}\boldsymbol{q_{t}}=slerp(q_{1},q_{2},t)=\frac{\sin(θ(1-t))}{\sinθ}\boldsymbol{q_{1}}+\frac{\sin(θt)}{\sinθ}\boldsymbol{q_{2}}
\end{eqnarray}
しかし2つ以上のキーがある回転の間を移動する場合、slerpでは補間の間にガタツキが生じます。ガタツキの例はこちらで分かりやすく紹介されています。これを避ける方法がEberlyさんにより提供されています。この方法では以下のように、クォータニオン\(a_{i}、a_{i+1}\)を\(q_{i}、q_{i+1}\)の間に導入します。
\begin{eqnarray}\boldsymbol{a_{i}}=\boldsymbol{q_{i}} exp[-\frac{log(\boldsymbol{q_{i}^{-1} q_{i-1}})+log(\boldsymbol{{q_{i}^{-1} q_{i+1}}}) } {4} ] \end{eqnarray}
3次スプラインによるクォータニオンの球面線形補間であるsquad(Shoemakeのクォータニオン曲線)を以下のように求めることで、上手く球面線形補間できます。
\begin{eqnarray}squad(q_{i},q_{i+1},a_{i},a_{i+1},t)=slerp(slerp(q_{i},q_{i+1},t),slerp(a_{i},a_{
i+1},t),2t(1-t)) \end{eqnarray}
逆クォータニオン
逆行列のように、あるクォータニオンに乗算したときに1になるクォータニオンのことを逆クォータニオンと言います。\(q=q_{0}+ \boldsymbol{q}\)と置くと、共役クォータニオンと掛け合わせたとき、
\begin{eqnarray}q\bar{q}=(q_{0}+\boldsymbol{q})(q_{0}-\boldsymbol{q})=(q_{0}^2+\boldsymbol{q・q},-q_{0}\boldsymbol{q}+q_{0}\boldsymbol{q}-\boldsymbol{q×q})=|q|^2 \end{eqnarray}
※\(\boldsymbol{pq}=-\boldsymbol{p・q}+\boldsymbol{p×q}\)を利用
以上より、逆クォータニオンはクォータニオンのノルムと共役を用いて以下のように表せます。
\begin{eqnarray}q^{-1}=\frac{\bar{q}}{|q|^2} \end{eqnarray}
デュアルクォータニオン
デュアルクォータニオンとは
先ほどはクォータニオンを使って回転を表現しましたが、回転に加え平行移動の表現も可能にするのがデュアルクォータニオン(クォータニオンの二次元ベクトル)です。3dcgのスキニングでは、従来のリニアブレンディングスキンニング(LBS)に代わる方法として、クォータニオンを用いたデュアルクォータニオンスキニング(DQS)があります。LBSにはcandy wrapper artifactと呼ばれる、曲げたときに体積が失われる問題が存在します。これは、LBSでは回転行列と移動ベクトルを並べた剛体変換を重み付けし、その線形和を各々の頂点に適用するのですが、この変換が剛体変換であることは保証されていないために発生する問題です。DQSはcandy wrapper artifactを避けてくれます。ちなみに計算コストはLBSの1.5倍未満程度のようです。
※DQSにも、joint-bulging artefactと呼ばれる、曲げたときに関節の周りに膨らみが生じるという問題があります。これを避けるために、LBSとDQSを組み合わせる方法や、scale付きのデュアルクォータニオンを使う手法、元の距離を維持するために修正を加える手法があります。最近の手法では、Disney Researchの回転中心スキニングがあります。
デュアルクォータニオンの定義
元の状態を\(s_{0}\)、変化後の状態を\(s_{tr}\)とします。どちらもクォータニオンの二次元ベクトルです。回転を表すクォータニオンを\(r\)、移動を表すクォータニオンを\(t\)とすると、以下のように表すことができます。tとrの乗算する順が気になるところですが、回転と移動を作用させる順序に限らずこの形になります。これは嬉しいですね。
\begin{eqnarray}s_{tr}=\{\begin{pmatrix}1&0 \\0&1\end{pmatrix}r+\begin{pmatrix}0&0 \\1&0 \end{pmatrix}tr\}s_{0}\end{eqnarray}
ここで、\(\begin{pmatrix}0&0 \\1&0 \end{pmatrix}\)と同じ性質を持つようなε(\(ε^2=0\))を定義します。そうしてデュアルクォータニオンを以下のように表すことにしました。
\begin{eqnarray}d=r+εtr\end{eqnarray}
εの固有ベクトルを変更しても問題ないので、ε→\(\frac{ε}{2}\)として再定義します。\(d=r+\frac{ε}{2}tr\)としたとき、
\begin{eqnarray}|d|^2=(r+\frac{ε}{2}tr)(\bar{r}+\frac{ε}{2}\bar{t}\bar{r})=r\bar{r}+\frac{ε}{2}(tr\bar{r}+\bar{t}\bar{r}r) =1+(t+\bar{t})ε=1\end{eqnarray}
※回転クォータニオンrのノルムは1、tは移動量よりi、j、k成分しかないので\(\bar{t}=-t\)が成り立つ
デュアルクォータニオンとその共役の積が1となり、\(d\)は単位デュアルクォータニオンとなることが分かります。
参考
中国茶
私の友人が中国茶にハマってサークルを作ったので最近よくお邪魔しています。
9月は中華街にある悟空茶荘さんに行きました。

お茶の入れ方等、丁寧に教えて下さるので無知の私でも楽しめました。
2時間程滞在し、5煎くらい飲んだと思います。
お茶は飲みやすく、月餅も美味しかったです😊
11月は三越前のコレド室町にある王德傳さんに行ってきました。台湾茶のお店で、内装やメニューも可愛かったです。お菓子がついてきて、左からパイナップルケーキ、お団子、芋羊羹で、どれも美味しかったです😄
お茶の方は冷たい凍頂烏龍茶、お砂糖なしタピオカ入りを注文しました。自分は冷たいお茶を注文しましたが、暖かいお茶の方は2煎楽しめるようです。ティーカクテルもあるそうです、また来たいな。

楽しかった!
UBOのメモリ配置について
GLSLの仕様上、UBOで使用する構造体のデータは、型のoffsetを決められているものに合わせて定義しなければならない。問題がある事例(適当に構造体を定義するとどうなるのか)はDealing with memory alignment in Vulkan and C++が分かりやすかった。構造体の変数の順序を変更すると、表示結果が変わっている。
UBOのメモリレイアウト仕様
以下のように決まっている。(※32ビット浮動小数点の場合、N=4byte)

仕様はstd140の仕様に記載されている。ユニフォームブロックのメモリレイアウト@ゲームプログラマの小話も分かりやすかった。
上手くいく場合
offsetが、mat4の 4N = 16 の倍数を満たしている
struct UniformBufferObject {
glm::mat4 model; #Size : 4byte×4×4=64byte Offset : 0
glm::mat4 view; #Size : 4byte×4×4=64byte Offset : 64
glm::mat4 proj; #Size : 4byte×4×4=64byte Offset : 128
};
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
上手くいかない場合
offsetが、mat4の 4N = 16 の倍数を満たしていない
struct UniformBufferObject {
glm::vec2 foo; #Size : 4byte×2=8byte Offset : 0
glm::mat4 model; #Size : 4byte×4×4=64byte Offset : 8
glm::mat4 view; #Size : 4byte×4×4=64byte Offset : 72
glm::mat4 proj; #Size : 4byte×4×4=64byte Offset : 136
};
layout(binding = 0) uniform UniformBufferObject {
vec2 foo;
mat4 model;
mat4 view;
mat4 proj;
} ubo;
解決策
これを修正するためには、C++11で導入されたalignasを使って位置を合わせる必要がある
struct UniformBufferObject {
glm::vec2 foo; #Size : 4byte×2=8byte Offset : 0
alignas(16) glm::mat4 model; #Size : 4byte×4×4=64byte Offset : 16
glm::mat4 view; #Size : 4byte×4×4=64byte Offset : 80
glm::mat4 proj; #Size : 4byte×4×4=64byte Offset : 144
};
alignasを使わない方法
alignasを使わなくても GLM_FORCE_DEFAULT_ALIGNED_GENTYPES を定義する方法がある。しかし問題点が2つあったので、この方法はやめた。
#define GLM_FORCE_RADIANS #define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES #include <glm/glm.hpp>
問題1:無駄なスペースが増える
GLMの仕様を見ると、自動で16byte基準で整列してくれるような説明があった。
#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
#include <glm/glm.hpp>
struct MyStruct
{
glm::vec4 a; #Size : 4byte×4=16byte Offset : 0
float b; #Size : 4byte×1=4byte Offset : 16
glm::vec3 c; #Size : 4byte×3=12byte Offset : 32
};
void foo()
{
printf("MyStruct requires memory padding: %d bytes\n", sizeof(MyStruct));
}
>>> MyStruct requires memory padding: 48 bytes
#include <glm/glm.hpp>
struct MyStruct
{
glm::vec4 a; #Size : 4byte×4=16byte Offset : 0
float b; #Size : 4byte×1=4byte Offset : 16
glm::vec3 c; #Size : 4byte×3=16byte Offset : 20
};
void foo()
{
printf("MyStruct is tightly packed: %d bytes\n", sizeof(MyStruct));
}
>>> MyStruct is tightly packed: 32 bytes
調べたところ、mat2は16byte、mat3は48byte、mat4は64byte、と16の倍数になるようにメモリ配置されていることが分かった。しかしこの仕様では、場合によっては空バイトが増えてしまう。メモリの節約方法で後述した。
問題2:ネストされた構造体では使えない
また、GLM_FORCE_DEFAULT_ALIGNED_GENTYPES を定義したとしてもネスト化された構造体ではこの整列は適用されない。よってUBOのメモリレイアウトの定義より、Foo2のoffsetは 4N = 16byte で無ければならないのに対し、 GLM_FORCE_DEFAULT_ALIGNED_GENTYPES を使った場合は8byteになってしまう。
struct Foo {
glm::vec2 v;
};
struct UniformBufferObject {
Foo foo; #Size : 4byte×2=8byte Offset : 0
Foo foo2; #Size : 4byte×2=8byte Offset : 8
glm::mat4 model; #Size : 4byte×16=64byte Offset : 16
glm::mat4 view; #Size : 4byte×16=64byte Offset : 80
glm::mat4 proj; #Size : 4byte×16=64byte Offset : 144
};
struct Foo {
vec2 v;
};
layout(binding = 0) uniform UniformBufferObject {
Foo foo;
Foo foo2;
mat4 model;
mat4 view;
mat4 proj;
} ubo;
この場合、alignas(16) Foo foo2 としなくてはならないことになる。こういった事例がある以上、UBOにはalignasを追記しておくのが安全だと思う。
理想
struct Foo {
glm::vec2 v;
};
struct UniformBufferObject {
alignas(16) Foo foo;
alignas(16) Foo foo2;
alignas(16) glm::mat4 model;
alignas(16) glm::mat4 view;
alignas(16) glm::mat4 proj;
};
メモリの節約
以下のように構造体を定義する場合、本来であれば、float:4byte、vec3:12byteで、 4+12×3 = 40byte で済むものが、offsetを揃えることで64byteになってしまう。空きメモリは24byteとなって勿体ない。
struct UniformBufferObject {
alignas(4) float foo; #Size : 4byte×1=4byte Offset : 0
alignas(16) glm::vec3 A; #Size : 4byte×3=12byte Offset : 16
alignas(16) glm::vec3 B; #Size : 4byte×3=12byte Offset : 32
alignas(16) glm::vec3 C; #Size : 4byte×3=12byte Offset : 48
};
| ✓ | 空 | 空 | 空 | ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | 空 |
ここで、float型の変数fooを最後に持ってくると、48byteにすることができる。
struct UniformBufferObject {
alignas(16) glm::vec3 A; #Size : 4byte×3=12byte Offset : 0
alignas(16) glm::vec3 B; #Size : 4byte×3=12byte Offset : 16
alignas(16) glm::vec3 C; #Size : 4byte×3=12byte Offset : 32
alignas(4) float foo; #Size : 4byte×1=4byte Offset : 44
};
| ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | 空 | ✓ | ✓ | ✓ | ✓ |
参考
ANGLEを使ってOpenGL ES をVulkanに変換して動かす
ANGLEとは
ANGLE(Almost Native Graphics Layer Engine)は、OpenGL ES API呼び出しを各プラットフォームで利用可能なハードウェアでサポートされたAPIの1つに変換してくれる。最初はWindowsでWebGLレンダリングする方法として始まったらしい。現在のサポートはこちらで確認できる。
目標
Windowsで、OpenGL ESをそのままVulkanで実行できるようにしたい。
必要なツール(2021年8月時点)
VisualStudio 2019、Windows 10SDK、Python3.9、GLFW、ANGLE、chromiumのdepot_tools
導入手順
1 gclientやGN、ninjaが使えるように、depot_toolsをこちらからダウンロードする
2 depot_toolsへのパスを通す(※ここではPythonのパスより前に設定する)
3 VisualStudio 2019、Windows 10 SDKが利用可能か確認
4 ソースを取得する
set DEPOT_TOOLS_WIN_TOOLCHAIN=0 git clone https://chromium.googlesource.com/angle/angle cd angle python scripts/bootstrap.py gclient sync git checkout master
5 ninjaビルド用のディレクトリを作成する
gn gen out/Debug
ここではビルド前に様々なビルドオプションを設定することが可能。
6 ninjaでビルドする(時間がかかります)
autoninja -C out/Debug
ここではVisual Studioソリューション生成して、ビルドすることも可能。
7 out/Debugに以下のライブラリが生成されていることを確認
libEGL.dll
libEGL.dll.lib
libGLESv2.dll
libGLESv2.dll.lib
8 GLFWをダウンロード
ドキュメントによると、ANGLEのバックエンドをVulkanに変更するにはEGL拡張のEGL_ANGLE_platform_angleを使えばよい。eglGetPlatformDisplayEXT(EGLenum platform, void native_display, const EGLint attrib_list) のようにして設定することが可能で、ANGLE内にサンプルコードもある。しかしGLFWを使っている場合は初期化の前に以下のように設定するだけで簡単にバックエンドを変更できる。
glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_VULKAN);
参考
自分が確認したところ、GLFWのWindowsパッケージではANGLEがサポートされていないようだったので、GLFWのmain branchから直接cloneしてコンパイルした。
9 Visual Studio 2019でプロジェクト作成&ANGLEから必要なものを持ってくる。自分は以下のように配置した
AngleTest └main.cpp .sln lib ├GLES2 ├include └ANGLEのincludeの中身 └lib └x64 └libGLESv2.dll.lib、libEGL.dll.lib └GLFW x64 └Debug └.exe
アプリケーションと同じ階層に以下の.dllファイルを配置する
libEGL.dll
libGLESv2.dll
absl.dll
libc++.dll
VkLayer_khronos_validation.dll
vulkan-1.dll
zlib.dll
10 Additional Include/Library Directory と Additional Dependenciesを以下のように設定する
Additional Include Directories
..\lib\GLFW\include
..\lib\GLES2\include
Additional Library Directories
..\lib\GLFW\lib
..\lib\GLES2\lib\x64
Additional Dpendencies
glfw3.lib
libEGL.dll.lib
libGLESv2.dll.lib
11 テスト用のOpenGL ESは以下の通り
#include <iostream> #define GLFW_INCLUDE_ES3 #define GL_GLEXT_PROTOTYPES #include "GLFW/glfw3.h" int main(){ int Width = 800; int Height = 600; //バックエンドにVulkanを選択 glfwInitHint(GLFW_ANGLE_PLATFORM_TYPE, GLFW_ANGLE_PLATFORM_TYPE_VULKAN); if (glfwInit() == GL_FALSE) { throw std::runtime_error("failed to init GLFW"); } glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); auto window = glfwCreateWindow(Width, Height, "App", nullptr, nullptr); if (!window) { throw std::runtime_error("failed to create Window"); } glfwMakeContextCurrent(window); glfwSwapInterval(1); while (glfwWindowShouldClose(window) == GL_FALSE) { glfwPostEmptyEvent(); glViewport(0, 0, Width, Height); glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.f, 0.f, 0.5f, 1.f); glfwSwapBuffers(window); glfwWaitEvents(); } glfwDestroyWindow(window); glfwTerminate(); return -1; }
12 Vulkanで動いているか確認したいのでRenderDocを使う
デフォルトではOpenGL ESエミュレータのフックが有効でキャプチャできないので、以下のように無効にしておく。
set RENDERDOC_HOOK_EGL=0
参考
13 実行結果


備考
- 元々OpenGLのプロジェクトを無理やりOpenGL ESにしようとしたとき、GLEWを使っていると初期化に失敗するので、GLES2/gl2.h、GLES2/gl2ext.hから直接OpenGL ESの関数を呼び出した。
参考
- 何か間違い、気になる箇所等あれば言って下さると嬉しいです