Houdiniでプラレール自動生成


![pras.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/26e260f2-7fd3-a55b-b7a1-f51263bf3891.png)
この記事は[Houdini Apprenticeアドベントカレンダー2022](https://qiita.com/advent-calendar/2022/happrentice)　3日目の記事です。
Houdiniで鉄道のレール作成する手法は昔からよくプロシージャルの題材として取り上げられてきました。
https://www.youtube.com/watch?v=J7KZ-CMTILk
SOPの基礎として、Copy to Pointや法線のコントロールなど効率的に勉強できる。そして最終調整としてレールのコース変更が上流で変更できて非常にプロシージャルと相性が良いからです。

今回は鉄道のレールをそのまま作るのではなくプラレールとして作ってみたら面白いのではないかと思い、チャレンジしてみました。
![prarail2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/53dbcb22-64db-b0ca-29ac-4add48493ef2.gif)

まずはプラレールの仕様が必要です。さっそく調べ始めてみます。

今回は空転ワークスさんの[この同人誌](https://kuten.booth.pm/)を購入。サイズ感やプラレールの作法を学びます。（最高の同人誌です非常に勉強になります）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/d5f3a423-d829-e55c-a205-ddae79dd85cb.png)

同人誌で得られた知識と、実際にプラレールを購入してモデリングをしていきます。
レールや橋脚のモデリングは今回は詳しく説明しません。寸法に応じて大雑把な形状を作成したら、EditノードやPolyDrawノードでゴリゴリ調整します。
![praa.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/6497b9b0-d051-782a-8aa2-b81bcc553b4d.png)
仕様が決まっているならプロシージャルで簡単にできないかと思いますが、長さとか円周の調整は簡単ですが、肝心の結合部やUターンレール、橋脚など最終的には手でモデリングを強行する必要があります。


### どのような考え方で作成するか

パーツが準備できたので、さっそく自動生成の仕組みを考えていきます。

プラレールシミュレータみたいなサイト、すでに無いかなと探したところ一応ありました。
https://w.atwiki.jp/plalayout/pages/80.html
あるにはあるのですが、、
![aruga.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/0a010672-d9d6-4266-328e-89cff52f4f67.png)
レールがひとつひとつハンコのようになっていて、もちろん手で繋げていく必要があります。
短いレイアウトならこれでもいいですが、大規模になると人間がする仕事ではなくなってくるでしょう。

パッと思いつく方法は３つほど考えられます。

- 案１：ルールを作って文字カラムで認識したパターンごとに配置する（もしくはLシステム）
- 案２：事前にテンプレートとなるルートを敷き、その中で通るルート以外を削除する
- 案３：ラインを生成し出来る限り近接線となるように法線方向を調整しながらcopy to pointでレールを配置していく

今回クリアしなければならない課題は２つあります

- レールのサイズと接続部の方向が決まっている
- ある程度ルートやレール種別の修正が後から簡単にできる仕組みにしたい

案１～３の中ではどれを選べばよいでしょうか。
案１でやるとして、文字カラムを例えば以下のようなルールで作ったとします

```
S1,S1,CL,CL,S1,CR... 
※S1:直線レール,CL:曲線レール左曲がり,CR:曲線レール右曲がり
```

確かにこちらの意図するルートを実現できますが、いかんせんルートの修正が面倒くさすぎます。ビルの窓を並ばせる時のようにピンポイントでの修正が必須だったりすれば別ですが、今回のようにレールがかなり連続して並ぶ場合、文字のカラム数が20個ぐらいを越えたところから、人間の目ではどこを修正すればどこが直るのか判別できなくなります。（途中のレールの種類を変更した場合、その後続のレール全てが影響してしまい、望むべきレールのレイアウト作成は非常に難しくなります。）
案１はやめておいたほうがよさそうです。

テンプレートとなるルート（例えば１畳プラレールのような）を用意して、その上を様々なルートを通るという案２はどうでしょう。
![ichijo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/ae6625bb-8d89-d583-dbba-12e541a911b3.png)
レールの数を極端に抑えて、イレギュラーケースを抑えればできるでしょう。しかしプラレールには、1/4直線、2倍直線、曲線も通常の曲線の他に外側曲線があり、そのうちの1種類でもルート内に混入するとテンプレートが破綻します。この案もあまり良くなさそうです。

案３は、近接線をどのように作るのかが課題ですが、ある程度プロシージャルに様々なルートを作成できそうです。というわけで今回は案３を採用します。

## 近接線をどのように作るのか

近接線を作成する方法はいろいろとありそうですが、今回はルート探索のエージェントとなるポイントを一つ作成して、ForEach内でループさせます。
まずスタート位置に１つの独立したポイントを置きます。その位置からガイドとなるルートに最も近づくルートはどれかを、ガイドラインのポイントとプラレールのゴール同士nearpointで判別します。判別したらプラレール１つ分一歩進んで（エージェントのpositionを移動させ）立ち止まり、ゴールの接続部がどの向きを向いているか確認、エージェントに次の向き先となる法線をオーバーライドします。ガイドラインでnearpointの対象となるポイントはループごとに削っていきます。この手順をガイドラインの最終到達点に行くまで繰り返します。Booleanで徐々に削っていく、Carveで徐々に削っていく、などの案もありましたが、Booleanで意図しない削りすぎの問題やCarveでの距離計算が面倒なので、事前にガイドラインをプラレール1つ分に近しい間隔でresampleします。そしてLoopごとに0ポイント目を除外していけば確実にガイドラインの最後尾にたどり着きます。ループ回数も明確になります。
文で書くとややこしいので図で説明すると、以下のようになります。

１.スタート地点にいるエージェント（赤い点）がガイドラインのターゲット（水色の点）に一番近づくにはどのルートを通るべき？
![step1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/f02adf97-4fcd-b650-9e00-43ddfe32ac4c.png)

２.１～３の終端と水色のポイントの距離を測ったら直進するのが良さそう
![step2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/0bb47968-5ac4-54bd-d4c6-f1242d51bfee.png)

３.エージェントを２のルートで一歩前進（注意：あくまでルートの先端に移動するのであって、水色の点に行くわけではない）
![step3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/8426c599-c923-b0ae-4f11-76cfe180b1f3.png)

４.ガイドラインを削る（次のターゲットは水色の点）
![step4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/83b7fc8d-262c-76b5-c8ff-3d7302d3f4cb.png)

１.エージェント（赤い点）がガイドラインのターゲット（水色の点）に一番近づくにはどのルートを通るべき？
![step5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/5826e742-74b4-b410-ebde-9bfe947bcab3.png)

２.１～３の終端と水色のポイントの距離を測ったら左折か直進するのが良さそう、
３.ただ、近いのは直線のルートなのでエージェントを２のルートで一歩前進
![step6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/64decd6b-7654-9d3c-e0df-e16539b42a02.png)

４.ガイドラインを削る（次のターゲットは水色の点）
![step7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/5c99374d-cd26-d0ef-36ac-9bb2d9cb7982.png)

１.エージェント（赤い点）がガイドラインのターゲット（水色の点）に一番近づくにはどのルートを通るべき？
![step8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/0f8a8567-ffc8-6af8-31eb-8b3a7b318f07.png)

２.１～３の終端と水色のポイントの距離を測ったら左折が良さそう、
![step9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/bcd54e6e-7fca-cdcc-97dd-694fe1ff711b.png)

この繰り返しをガイドラインの最後になるまで行います。
最終的に黒色のガイドラインは消えて、薄い赤色のラインが残ります。この薄い赤色のポイント間隔は完全にプラレールの距離と合致します。その代償として、運が悪いと最初にガイドラインで書いたルートとのずれが広がってしまうことになります。ただし、この問題はプラレールを物理的曲げない限り、現実でも起こり得る問題なので受け入れるしかありません。

このガイドラインのresampleの値は、けっこうルートによって追い越し追い越され、でバラつきが生じますが、逆にそれがコースに良い感じのノイズを加えてくれるのでこの案を採用してよかったと思います。

ネットワークは以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/3a24be68-2305-bbf8-eb1f-39d5a8808530.png)
[hip](https://drive.google.com/file/d/1suSMYr1SQZVp05fg3hOpGicBZMv7qnF3/view?usp=share_link)

ループの回数はForEachの前にデータ参照用のnullを置き、
```
npoints("../Loop_Count_Check/")-1
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/daf2f8ea-7b36-a578-70dc-3b2a854522c3.png)


回ループさせています。Deleteがやけに多いのはFeedBackで前のデータを残してループするため、消さなければならないところできちんと消さないとループ分同じデータが重複してしまうからです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/1c901f48-0f7a-c784-4d3a-5e6c0f473110.png)

## CopyToPoint

プラレールのサイズに応じたpolylineができたので、コピーしていきます。
それぞれのポイントにはclassが割り当てられているので、CopyToPointのPieceAttributeを有効にして配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/6fe9d869-1b4a-2579-db96-f4cfdfdc0bcb.png)

注意しなければならないのが、ここのPieceAttributeはPrimitiveAttributeで設定する必要があります。


## KineFXへの接続

コースが出来たら、ちょっとずらしたり無理やりくっ付けるためにひねりを加えたいことがあります。
ちょうどpolylineがあるのでKineFXに接続したら、より便利そうです。
機械系の剛体リグに関しては、以前書いた記事　[KineFX機能まとめ](https://qiita.com/jyouryuusui/items/cc4c7c529fed71f03df3#:~:text=CapturePackedGeometry)　内でも書きましたが、CapturePackedGeometryというノードを利用します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/ec54b6a5-1e46-8faf-d3fd-7913e6bbb6ac.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/d762cee1-a3b0-8b53-9400-507c87594a1c.png)

このノードは機械のように固いオブジェクトをジョイントに対して追従させます。。。が、気を付けなければならないポイントがあります。
ジョイントへの追従の設定とウェイト1を付与するためには、```@name=point_0```　のように手動での設定(Manual Capture)が必須です。（僕が機能を見逃している可能性があります。もし誰か知っている人がいれば教えてください）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/f4484a98-566d-614f-6960-128fa6d1e765.png)

この手動での設定が厄介で、上流でコースが変わってしまうと、また再設定を行わなければなりません。
完全にボトルネックになってしまいます。
このManualCaptureが空のままだと、以下の２つのAttribute(boneCapture)が付与されず、BoneDeformでエラーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/50db5bba-7b01-b598-bfb6-46d0b6309334.png)
このAttributeは普通のAttributeではなく、CaptureAttribunというものでpackされています。なので、中身の値を見るためにはcaptureattribunpackノードを使用する必要があります。
unpackすると以下のようにpointとdetailに値が入っていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/5c6c5786-6db1-18ee-19a5-81e1ad9e01b3.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/5dce76f4-0180-0cc0-b013-0dbb6c0eaa80.png)
一応[hipファイル](https://drive.google.com/file/d/1ui2z2sOmMl2GJT_LEZYDsgdAJ1792JNj/view?usp=share_link)


そんなときはCapturePackedGeometryノード内の処理をコピーして処理を改造します。まず、CapturePackedGeometryノードで右クリックをし、編集可能な状態にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/5b7f5e5d-6b8c-a071-6254-10dbb31cd40e.png)

ここのノードをコピーして、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/a77c8386-fb7c-5032-ded5-00011c7af81d.png)

このpack_from_multiparmという名前のWrangleを以下のように書き換えます。

```c++
float captdata_default[] = array(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1,1,1,1,1);
float data[] = array(1.0);

int k=0;
for(int i=0; i<npoints(0); ++i)
{
    string geogrp = itoa(i);
    string jointi = "@name=point_0";
        
    int prims[] = expandprimgroup(0, geogrp);
    int pt = expandpointgroup(1, jointi)[0];
    
    //string capt_name = string(point(1, "name", pt));
    string capt_name = "point_"+itoa(i);
    k = find(s[]@boneCapture_pCaptPath, capt_name);
    if(k < 0)
    {
        matrix capture_xform = pointtransform(1, pt);
        capture_xform = invert(capture_xform);
        float _capt_data[] = set(capture_xform);
        append(_capt_data, array(1,1,1,1));
        

        append(s[]@boneCapture_pCaptPath, capt_name);
        append(f[]@boneCapture_pCaptData, _capt_data);
        k = len(s[]@boneCapture_pCaptPath)-1;
    }
    
    int capt_prims[] = findattribval(0, "point", "__capture_pt", k);
    foreach(int capt_pr; capt_prims)
    {
        int idx[];
        float emptydata[];
        setpointattrib(0, "boneCapture_index", capt_pr, idx);
        setpointattrib(0, "boneCapture_data", capt_pr, emptydata);
        setpointattrib(0, "__visCd", capt_pr, {1,1,1});
        setprimattrib(0, "__captured", capt_pr, 0);
        setpointattrib(0, "__capture_pt", capt_pr, -1);
    }
    
    foreach(int pr; prims)
    {
        int idx[] = array(k);
        int prim_pt = primpoints(0, pr)[0];
        setpointattrib(0, "boneCapture_index", prim_pt, idx);
        setpointattrib(0, "boneCapture_data", prim_pt, data);
        if(haspointattrib(1,"Cd"))
            setpointattrib(0, "__visCd", prim_pt, vector(point(1, "Cd", pt)));
        else
            setpointattrib(0, "__visCd", prim_pt, chv("def_highlight"));
        setprimattrib(0, "__captured", pr, 1);
        setpointattrib(0, "__capture_pt", prim_pt, pt);
    }
}
```

BoneCaptureに認識させるためのキャプチャ基本情報をPointAttributeとDetailAttributeに格納しています。

これでBoneDeformが有効になり、全自動でボーンの設定まで行ってくれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/c1530336-8ff6-abcb-093c-876e4d2a0828.png)


## 残課題
### レールの種類
プラレールHDAとするにはまだプラレールの種類が足りていません。
問題は2分岐したり、2つが平行するレールの処理です。
この問題、ずっと悩んで出した結論は、2分岐のレールは分岐の片方だけを認識させて、もう片方は別のノードとしてHDAを結合する　というアイデアです。

あまりにも複雑な処理をするぐらいなら、簡単な処理を２つ繋ぎ合わせればいいのです。

### 電車を走らせるためのパスが無い
今回は時間が無かったので実装できませんでしたが、各パーツをmergeさせるところに
電車を走らせるためのレールの中心を沿ったpolylineを実装する必要があります。

### hipファイルの整理整頓
時間がない中で作ったので無駄な処理が残っているかもしれませんが、そこは勘弁してください。

# まとめ

今回はHoudiniでプラレールのレールを作成しました。まだ完全なHDAにはなっていないので今後も改良を続けたいと思っています。事前想定よりも大変だった事と、躓きポイントがいくつか判明したので、これに類似するようなプロジェクトのお役に立てれば幸いです。

# あとがき
6カ月前にプラレールの同人誌を購入し、実物のプラレールも20年ぶりぐらいに購入しました。
いやよくできている。プラレールって楽しい　というのを思い出しました。ちょうどその時期に地元のイルミネーションの展示スペース内で何かいいアイデアがないか？と相談があり、宮沢賢治の銀河鉄道の夜をプラレールで展示をする機会を頂きました（[2022年12月25日まで展示しています](https://yumenarie.com/)）。このHoudiniの仕組みはその際にレイアウトを検討するために3Dモデルがあったほうが便利だなということで作成を開始したのがきっかけです。結果的に活用できたかというと、そちらの展示準備が忙しすぎて、一部は動いたもののHDAまでは行きつきませんでした。ただ、このHDAが完成したら、こういった展示向けに事前にレイアウトすることが非常に簡単にできるんじゃないかと思ってます。
![ginga.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151589/662d517b-b039-0f8c-19d8-d16aafad07d9.png)