kick the base

Houdiniと、CG技術と、日々のこと。

Houdini: 指定した方向に対してUV展開を行う

今回のお題

「バラバラな向き・比率の長方形を一定の方向に沿ってUV展開できない?」という話が出たので実装してみました。言葉では難しいので画像でなんとなくご理解いただけるのではと思います。

f:id:kickbase:20190219013425j:plain

今回ポイントとなるのは以下のふたつ。

  • 長方形の長手方向(長辺)を判定する
  • ベクトルの回転をコントロールする

以上。やっていきましょう。

サンプルファイル

サンプルhip

ノード解説

順を追って見ていきましょう。

ランダムなサイズ・方向の長方形を複数作る

f:id:kickbase:20190219200435j:plain

特に難しいところはありませんね。

ランダムサイズはscaleアトリビュート、ランダム回転をorientアトリビュートで設定しています。

ランダムに回転させるとき、ぼくはAttribute Randomize SOPでクオータニオンを作成することが多いですが、今回はWrangleでスケール調整していたのでついでにワンライナーで書いています

@scale = set(fit01(rand(@ptnum), 1, 3), 0, fit01(rand(@ptnum+1), 1, 2));
p@orient = quaternion(radians(rand(@ptnum) * 360), {0, 1, 0});

この部分はFile SOPで置き換えてジオメトリを読み込んでもよいですし、HDA化して入力として受け取ってもよいでしょう。

実装前に方法を言語化する

ここが最も大切な工程です。なんとなくそれっぽいノードを繋いでいってもうまくいかないことが多いので、ここは時間をかけましょう。

  1. 長方形をひとつづつ取り出す
  2. 長方形の長手方向(長辺)を取り出す
  3. X座標{1, 0, 0}と長辺がなす角を算出する
  4. なす角の分だけ逆方向に回転する
  5. UV展開をする
  6. 元の形状にUV情報を転写する

これくらい分割すると、それに対応するノードも見えてきます。

行けそうですね。

長方形をひとつづつ取り出す

これはFor-Each Connected Pieceでいいでしょう。

長方形の長手方向(長辺)を取り出す

f:id:kickbase:20190219200522j:plain

ここは少し工夫が必要です。

Convert Line SOP

長方形をラインに変更します。

Mesure SOP

TypeをPerimeterにして、辺の長さを取得します。

プリミティブアトリビュートPerimeterに格納されます

Sort SOP

Primitive SortをBy Attribute にし、AttributeをPerimeterとします。これでPerimeterが大きい順にPrimitive Numberがソートされます。

Blast SOP

Groupを0に、Group TypeをDelete Non Selectedにします。これで長辺が取得できました。

Groupを0にすると一見ハードコードに思えますが、事前にSortしてるので一意的に決定します

X座標と長辺がなす角を算出する

f:id:kickbase:20190219200623j:plain

VEXで書いちゃいましょう。Attribute Wrangle SOPを作成し、Run OverをDetail(only onece)に、ノード名をattribwrangle_dir_radにします。

正規化されたベクトル同士の内積はcosθになるので、内積の値をacos関数に渡すことでなす角θを取得します

v@dir = normalize(point(0, "P", 1) - point(0, "P", 0));
@rad = acos(dot(@dir,{1,0,0}));

なす角の分だけ逆方向に回転する

ふたつの工程に分けて説明します。

長辺の情報を長方形に転写する

Attribute Wrangle SOPを作成し、Run OverをDetail(only onece)に、ノード名をattribwrangle_cross_set_radにします。ここで逆回転用の角度radアトリビュートを生成しています。

転写という言葉を使っていますが、原理としてはdetail関数で入力1に繋いだ長辺のアトリビュートにアクセスし、自身(長方形)のアトリビュートに代入しています。

vector crossprod = cross(detail(1, "dir"), {1,0,0});
if(crossprod.y > 0){
    @rad = detail(1, "rad");
}else{
    @rad = radians(360) - detail(1, "rad");
}

上記コードで外積を使っていますが、この理由は別に解説用のネットワークを組んだのでそちらで詳しく後述します。

ジオメトリを回転する

f:id:kickbase:20190219200658j:plain

VOPで作りましたがもちろんWrangleでも作れます。

※ Point Wrangleで実装した場合のコードはこちら

@P = qrotate(quaternion(detail(0, "rad"), {0, 1, 0}), @P);

これですべての長方形をX軸方向へ向けることができました。

UV展開をする

UV Texture SOPを作成し、Texture TypeをOrthographicにします。Attribute ClassをPointにしていますが、デフォルトのVertexでも大丈夫です。

元の形状にUV情報を転写する

f:id:kickbase:20190219200756j:plain

ご覧の通り長方形が全部真横を向いているので、元のランダムな角度の形状にuvアトリビュートを転写します。Attribute Copy SOPを利用しましょう。

これで完成です!

なす角の算出と内積・外積についての詳しい解説

解説用のGeometry、LINE_ROTATIONを用意しました。

f:id:kickbase:20190219200824j:plain

merge1ノードにディスプレイフラグを立てて再生してみると、0~360度までラインが回転します。このラインを毎フレーム逆回転させて、X軸方向から移動しないモーションが最終目的です。

f:id:kickbase:20190219200846g:plain

失敗バージョン

逆回転用のアトリビュートradを設定するために、pointwrangle_rad_ngノードに下記コードを書きました。

  1. 回転しているラインとベースとなるライン(X軸方向)のベクトルを取得
  2. 上記のベクトルから内積を取得
  3. 内積をacos関数に渡すとなす角が得られるので、それをマイナス1倍して逆回転用の角度を取得、それをradに代入
v@dir_rot = normalize(point(0, "P", 1) - point(0, "P", 0));
v@dir_base = normalize(point(1, "P", 1) - point(1, "P", 0));
float d = dot(@dir_rot, @dir_base);
float r = acos(d)* (-1.0);
@rad = r;

問題なさそうですが、実はこれNGなんですね。

Point VOPで回転させてみましょう。(回転用のノードは先に説明したVOPと全く同一です)

f:id:kickbase:20190219200919g:plain

ご覧の通り、0~180度の範囲ではちゃんと逆回転が効いていてリセットされてますが、180~360度の範囲ではラインが動いてしまっています。これの理屈を簡単に説明しましょう。

f:id:kickbase:20190219201641j:plain

画像の太い赤線がcosθの値です。45度と315度で同じ値になっていますね。つまりcos関数は0~180度までしか一意的に判断できません。

それを解決するために外積を利用します。

f:id:kickbase:20190219200946g:plain

上記動画を見てのとおり、外積ベクトル(黄色いベクター)のY値が0~180度の間はマイナス、180~360度の間はプラスになっています。この値を使えば逆回転用のアトリビュートradを0~360度の間で設定できそうです。

成功バージョン

f:id:kickbase:20190219201030g:plain

ご覧の通り0~360度の範囲で逆回転がかかり、ラインが移動していないのがわかるかと思います。

VEXは下記の通りです。外積のY値をもとにradの値を変更しています。

vector dir_rot = normalize(point(0, "P", 1) - point(0, "P", 0));
vector dir_base = normalize(point(1, "P", 1) - point(1, "P", 0));
float d = dot(dir_rot, dir_base);
float r = acos(d)* (-1.0);
vector crossprod = cross(dir_rot, dir_base);
if(crossprod.y < 0){
    @rad = r;
}else{
    @rad = radians(360) - r;
}

まとめ

今回は少々数学的なお話が出てきましたが、ベクトルを自由に回転させることはHoudiniではよく使うテクニックなので慣れておくといいかと思います。