今更人には聞けない回転のお話
今日は いろんな回転の方法 について考えていきます。
という訳でTransformSOP禁止縛りです。
- 何故か?
→ある程度理解を深める為です。 - Wrangle苦手?
→最初はコピペでいいです、写経なんかしなくていいですとにかく慣れましょう。
ぶっちゃけCopilotやGeminiに考えてもらってもいいです。
記事にしろAIにしろ読んで実行して理解していずれ自分の実力につなげて下さい。 - なんでVOPじゃないの?
→操作と説明の簡便さの為です。コードは記事に記述しやすいですので。
クォータニオンで回す
dihedralで測ってqrotateで回す
例えばLineSOPで縦の一本線を出した状態のものを手前(Z+方向)に回転させたい場合

PointWrangleでこの様に書けば
vector A = {0, 1, 0};
vector B = {0, 0, 1};
vector4 qut = dihedral(A, B);
@P = qrotate( qut, @P );
dihedralで回転のクォータニオンが得られ
qrotateでクォータニオンを使った回転が行われます。
この回転を表すクォータニオンというものがどういうものかを真面目に考えると大変なのでとりあえず
回転量と回転軸
で表される回転として覚えておくと楽です。
wrangle内でquaternionを定義する際の引数がそうなっています。
https://www.sidefx.com/ja/docs/houdini/vex/functions/quaternion.html
dihedralの弱点
では次に Template Head を出して X軸に90度回転させてうつ伏せにしてみて下さい。

これを頭頂部を手前にしたまま仰向けにしたい場合、先程の様なwrangleで出来るでしょうか?
PointWrangleを出して下につなげてこの様に書いてみましょう。
YマイナスをYプラスにしたい訳ですからこう。
vector A = {0, -1, 0};
vector B = {0, 1, 0};
vector4 qut = dihedral(A, B);
@P = qrotate( qut, @P );
するとこうなります。

仰向けにはなりましたが頭頂部は向こうを向いていますね。
頭頂部をこちらに向けたまま仰向けにしたい場合、つまり
X軸回転で180度回すのではなくZ軸回転で180度回すればいいのですが
そうしたい場合はどうすれば良いのでしょうか?
dihedralは2つのベクトルからクォータニオンを導き出します。
その際あくまでAをBにする回転を出すだけで回転軸は指定できません。
つまり向きは指定出来ても姿勢までは制御出来ないのです。
軸を指定して回転させたい場合は下記の様にします。
vector4 qut = quaternion( radians(180),{0,0,1} );
@P = qrotate( qut, @P );
回転量と回転軸を指定してクォータニオンを作成しqrotateで回転させてやれば良いのです。

これで頭頂部が手前で且つ仰向けになりました。
ちなみにMatch Axisを右クリックして Allow Editing Contents をして潜ると
その中にPointWrangleがあり
vector4 q = dihedral(chv('from'), chv('to'));
v@rot = degrees(quaterniontoeuler(q, 0));
中でdihedralが使われていますので指定したベクトル自体は回転してくれるのですが
回転させる為の軸は指定出来ません。
@Nと@upを使った回転
次にこれまた初歩的なお話ですがdihedralみたいな弱点は無いです。
@Nと@upなどを使って Copy To Point で複製されるものの向きをコントロール出来ます。
@NがZ軸+、@upがY軸+に揃う様に回転させられるのです。
Template Head と Sphere と CopyToPoints を下図の様につなげてみて下さい。

すると

この様に正面(Z軸+)正面から見るときれいに頭のZ+が法線方向に沿って配置されているのが分かります。
しかし正面(Z軸+)以外の方向から見ると

頭頂部がバラバラな方向を向いていることが分かります。
NormalSOPなどで明示的にPointアトリビュートの @N を作っていない状態でも暗黙的にPointWrangle内で使用することが出来ます。
しかしジオメトリスプレッドシートなどで確認出来ないのは不便なので作っておいたほうが実際分かりやすいです。
@upはWrangleやPolyFrameなどで生成しないといけない様です。
ここでPointWrangleを出して下につなげて
v@up = {0,1,0};
ではここで
「球から生えたすべての頭の頭頂部を外側に向けて顔の正面を右斜上に向けたい」
といった場合はどうすればいいでしょうか?
CopyToPointの二番目の入力に入れる点の @N と @up を制御すればいいということは
想像に難くないと思います。
Sphereの下にPointWrangleを挟んで

@N と @up を入れ替えることでそれっぽいところまで来ました。
「顔の正面を右上に向ける」という部分をどう考えるかというところですが
@N を @up で反時計回りにqrotateで回転させれば良さそうです。
という訳でPointWrangleの中身をこの様にします。
v@up = v@N;
v@N = {0,1,0};
vector4 qut = quaternion( radians(-45), v@up );
v@N = qrotate( qut, v@N );
すると

想定した結果が得られました。
角度を微調整したければ -45 と指定しているところを chf('rot') の様にパラメータ化してあげれば調整出来る様になります。
じゃぁ
「この頭をノリノリでヘッドバンキングさせたい」
といった場合はどうすれば良いでしょうか?
「無茶言うな、ヘドバンさせるには回転させる為のX軸に相当する軸の情報がないじゃんか」
と思われた方もいらっしゃるかもしれません。
無ければ作れば良いんですよ、X軸なんて。
2軸に垂直な軸を計算してくれる便利な外積という概念がありまして
数学的で使われる演算はだいたいのプログラム言語には用意されていてwrangleにもあります。
crossという名前の関数です。
v@up = v@N;
v@N = {0,1,0};
vector4 qut = quaternion( radians(chf("y_rot")), v@up );
v@N = qrotate( qut, v@N );
vector xAxis = cross(v@N,v@up);
vector4 qutX = quaternion( radians( 30*sin(@Frame*0.1) ), xAxis );
v@N = qrotate( qutX, v@N );
v@up = qrotate( qutX, v@up );
カーソルキーの上を押して再生すると首を縦に振るような動きが確認出来ます。

それぞれの頭の動きにばらつきを持たせたいなら@Frame*0.1のあとに@ptnumをシードにしたrand関数をたしてやればばらつきが出てきます。
@N と @up の二軸が定まっていれば回転は一意に定まります。
それらをどの軸でどれくらいの量回すかを把握出来ていれば
回転の制御はそんなに難しいことはないでしょう。
ちなみにquaternionを @orient としてやるとそちらの方が優先順位が高くなります。
こちらにその辺りの仕様が書かれていますので一度確認してみても良いでしょう。
回転のアトリビュートの優先順位は下記の様になっています。
@pivot > @transform(matrix) > @orient(quaternion) > @N & @up > @rot(quaternion)
VATのRigidBodyは @pivot と @orient が必須です。
あとアトリビュートの型を定義する時の1文字ですが割と忘れがちなので表を貼っておきます。

特にuとかpを自分はよく忘れます。
これもSideFX社のこちらのページに記述されていますので御覧下さい。
Houdinist古株の方、辞書型も1~2年前に追加されてますよ、忘れずに見ておいて下さいね。
予約語的なアトリビュート名のリストもなにげに役立ちますね。
クォータニオンについてはこちらのけんちょんさんの記事でとても分かりやすく紹介されているので興味がある方は一読されてみると良いでしょう。
Lookat ってどう?
lookatという関数もあります。
名前からしてカメラ関係の関数なんだろうなというのは推測できます。
https://www.sidefx.com/ja/docs/houdini/vex/functions/lookat.html
説明には
マイナスZ軸がトランスフォーメーションのベクトル(to-from)に向くための回転マトリックスまたは回転角度を計算します。
とあり返り値はmatrix3かvectorです。
カメラマン役としてTemplateHead、対象物としてSphereを使って見てみましょう
ノードとパラメータはこんな感じで組んでみます。
頭をY軸180度回しているのはマイナスZ軸がカメラの見てる方向とされているのでそうしました。

wrangleは写経するとミスる人が出てくるのでコピペ用に書いておきます。
matrix3 m = lookat({0,0,0},point(1,"P",0),{0,1,0});
@P *= m;
座標2つとアップベクターを使ってMatrix3を生成してくれるので
それで回す感じですね。
Sphereを動かすと頭のポイントの座標がマトリックスによって回されているのがわかります。
Vectorで返り値を取っても良いのですが回転順序を指定して各軸の回転を考慮しないといけないので結構めんどくさいです。
dihedralと似てますがdihedralはベクトル2つを入力としてlookatは座標2つとroll or upを入力します。
Matrixで回す
既に前述のlookatの項目でmatrixを使ってますが簡便に書けて良いですね。
「Matrix(行列)とは何ぞや?」 という人の為に雑な説明をすると
${\tiny 縦横に並んだ数字の塊を行列というのですが掛け算の順序が逆になると結果が違ったり}$
${\tiny 単位行列、ゼロ行列、正方行列、逆行列、トレースや固有値、固有ベクトルなど様々な概念があり}$${\tiny 普通の数字とは違った独特な計算方法で連立方程式の解を効率的に求めたり}$
${\tiny ベクトルや点の座標変換(回転、拡大縮小、平行移動など)を表すのに用いられたり}$${\tiny ビッグデータの解析、画像処理、機械学習の分野で、データの構造を表すのに用いられたりします。}$${\tiny 行列を理解することは、現代科学技術の多くの基盤を理解することにつながります。 }$
まぁ「便利なやつ」なんで扱い方を覚えておいて損はないです。
単位行列を作って回す
matrix m = ident(); // 単位行列を作って
float rot = radians( chf("rot") );
rotate(m,rot,{0,1,0}); // それを回転させる
@P *= m; // 回転された行列を使って点を回転させる
結果はこう

マトリックスを回転させる関数 rotate を使っています。
polyframeで作ったtangentuで回す
人の頭がくるくる回ってるだけだとあまり芸が無いのでちょっとSciFiチックにやってみましょう。
まずは下準備として template_head の口の中を消してRemeshしてDivideで六角形のメッシュにします。

Facet で 分割して UVTexture でUVを生成してUVを元に PolyFrame で tangentu を生成します。
分割された Primitive を tangentu を軸にして回転させたいのです。

法線を作って押し出してパックして法線を3つ上から引っ張ってきます。
事前準備はここまでです。

PointWrangleでmatrixを作りrotateで回しsetprimintrinsicでtransformにそのmatrixを設定すれば出来上がりです。

float angle = max( 0, min(@Time*2.5+@P.y*20-7,3.14159*2));
vector axis = normalize( v@tangentu );
matrix3 m = ident();
rotate(m, angle, axis);
setprimintrinsic(0,'transform', @ptnum, m, 'set');
@P += @N*sin(max( 0, min(@Time*1.7+@P.y*20-7,3.14159)))*chf("offset_outside");
シーンを再生させるとこの様に各Primitiveが回転しながら動いているのが確認出来ます。

まとめ
回転の方法をいくつかご紹介しました。
単純なTransformでは対応出来ない複雑な回転が必要になる局面も多々あるので
これらの様なやり方も覚えておいて損はないでしょう。
最後の例だけ少し複雑なのでhipを用意しましたのでよかったらご活用下さい。
今年もあと僅かになって参りましたが皆さまにとって良い締めくくりの時期となりますよう心よりお祈り申し上げます。
どうぞお体に気をつけて、良い年末をお過ごしください。
来年も新しい挑戦と学びを皆さまと共有できることを楽しみにしております。



