フルプロシージャル社寺建設

お久しぶりです。最近は積んでるゲームで忙しいため、Hoduiniをあまりできていませんが、過去に作成した社寺建築HDAについて紹介できればと思います!

## 社寺建築HDAの紹介

日本古来の木造建築をHoudiniで自動作成できるようしたHDA群です。ノードとしては以下のものを含んでいます。
- 床・柱・屋根を構築するためのベーストポロジージェネレーター
- 床ジェネレーター
- 柱ジェネレーター
- 屋根ジェネレーター
- ベーストポロジーを他のベーストポロジーとアラインメントするノード

ベーストポロジーを作成後、床・柱・屋根のHDAを組み合わせることで好きな形の社寺建築を作れるようにしています。

https://twitter.com/TrsVrc/status/1518987931943247872

またアラインメントをするノードを使うことで複雑な外観にできます。

https://twitter.com/TrsVrc/status/1522776832432492544

今回はHDAを作成する際に考えていたことを大まかに説明できればと思います。

※ HDAを含むhipファイルは以下のGitHubのリンクからダウンロードできます。個人利用の範囲でお使いください。
https://github.com/TrsNium/riplc


## 1. 屋根ジェネレーターHDAについて

古建築入門やインターネット上の書籍を読み、どのような手順で建築されているのかを知るところから始めました。手順さえ分かれば、Houdini上でいくらでも再現できるだろう考えたためです。

https://twitter.com/TrsVrc/status/1478056534949384192


### 木組み
 
手始めに軒部分の意匠を再現するところから始めました。現存する社寺建設は、全て同じ意匠ではなく個々で異っています。その差が最終的な外観の面白さや美しさに繋がると考えたため、パラメータで木組の意匠を変更できるようにしています。

https://twitter.com/TrsVrc/status/1484802973641232384

Houdiniで実装する際の手順としては以下の通りです。
1. パラメーターから木組の設計図(数字列からなるディテールアトリビュート)を作成する
1. 設計図を元に意匠毎に処理を分岐させる
1. 設計図と意匠用の処理をもとにポリゴン化する

設計図を一番初めに作ることにより、後に拡張性を持たせることができます。例えば「尾垂木」と呼ばれる意匠は後々付け加えた物ですが、下流のノードに殆ど影響なく付け加えれました。

### 屋根
次に取り掛かったのは屋根の作成です。屋根と言っても、「入母屋根」、「方形屋根」や「寄棟屋根」などの種類があります。「方形屋根」と「寄棟屋根」に関しては、Houdiniの便りなノードを使うことで直ぐに再現可能です。

![新規 ビットマップ イメージ3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177955/3b8366df-b52f-6303-5e9c-09152815abf3.jpeg)

手順としては以下の通りです
1. divideノード等を用い、shared edgeのないジオメトリを用意する
1. polyexpand2dを用い内側にオフセットしたジオメトリを作成する
    1. この時のオプションでOutputを*Offset Surfaces*にし、Local Attributeの*Edge Distance Attribute*で頂点にエッジからの距離情報を入れます。
    ![新規 ビットマップ イメージ4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177955/707ce561-9387-a392-45c2-a6028681c224.jpeg)
1. 先ほど作成した頂点アトリビュートをpointへpromoteします
1. 最後にwrangle等をつかいy軸方向にpointをbumpします

「入母屋根」の作成に関してはsakana0147さんの動画を参考に作成しています。
<iframe src="https://player.vimeo.com/video/313743656?h=dbe46b46fc" width="640" height="360" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
<p><a href="https://vimeo.com/313743656">[Houdini] Procedural roof test</a> from <a href="https://vimeo.com/user7352198">sakana0147</a> on <a href="https://vimeo.com">Vimeo</a>.</p>
説明が冗長になるので、実際にhipファイルを開いてみていただければと思います:bow:

### 2. 床/柱生成hdaについて

ジオメトリーを生成する部分は特筆して書くことはないです。
代わりに、どのベースジオメトリから床/柱をインタラクティブに選択する方法について書きます。

https://twitter.com/TrsVrc/status/1522124302694244352?s=20&t=L-EcD9_qsnB72CIOv4llZg

手順としては以下の2つになります。
1. 入力のベースジオメトリから、ベースジオメトリを選択するパラメータを作成する。
1. パラメータの値からどのベースジオメトリが選択されているかアトリビュートとして書き出す

#### 入力からパラメータを作成する
hdaのパラメータを動的に変えるために、HDAの入力に変更があった際にコールバックされるスクリプトを記述しています。
コールバックの設定はOperator type Properties > scriptsの欄から編集することが可能です。
![新規 ビットマップ イメージ5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/177955/d078909a-fa08-065e-1977-9c26cba4a4f5.jpeg)

コードは下記のようになっています。ベースジオメトリのディテールアトリビュートを読み出し、ベースジオメトリが単数の場合や前の処理ので既に選択されていた場合には、パラメータを削除します。それ以外の場合は、新しくパラメータを作成します。python上でのパラメーターの操作は[hou.ParmTemplateGroup class](https://www.sidefx.com/ja/docs/houdini/hom/hou/ParmTemplateGroup.html)のページが参考になります。

```python
newnode = kwargs['node']
newnode = newnode.path()
node = hou.node(newnode)

parm_template_group=node.parmTemplateGroup()
parm = parm_template_group.find("sub_ids")

def _delete_not_necessary_param():
    parm_template_group = node.parmTemplateGroup()
    if parm_template_group.find("ids"):
        parm_template_group.remove("ids")
    node.setParmTemplateGroup(parm_template_group)

def _create_param(node, ids, sub_ids):
    parm_template_group = node.parmTemplateGroup()
    sort = sorted([(f"{sub_id}:{id}",idx) for idx, (id, sub_id) in enumerate(zip(ids, sub_ids))])
    sort_idx = [idx for (_s, idx) in sort]
    id_parm = hou.MenuParmTemplate(
        "ids",
        "select ids",
        menu_items=([f"{ids[idx]}" for idx in sort_idx]),
        menu_labels=([f"{s}" for s, _idx in sort]),
        default_value=0,
        icon_names=([]),
        item_generator_script="",
        item_generator_script_language=hou.scriptLanguage.Python,
        script_callback_language=hou.scriptLanguage.Python,
        is_button_strip=1,
        menu_type=hou.menuType.StringToggle
    )
    parm_template_group.insertBefore("reload", id_parm)
    node.setParmTemplateGroup(parm_template_group)

def _get_subids(topology_node, id):
    topology_geometory = topology_node.geometry()
    prims = topology_geometory.prims()
    for prim in prims:
        if id==prim.attribValue("id"):
            return prim.attribValue("sub_id")

def _get_prev_process(topology_node, id):
    topology_geometory = topology_node.geometry()
    prims = topology_geometory.prims()
    for prim in prims:
        if id==prim.attribValue("id"):
            return prim.attribValue("prev_process")        
    
topology_node = node.inputFollowingOutputs(1)
if topology_node:
    topology_geometory = topology_node.geometry()
    prev_selected = topology_geometory.intListAttribValue("prev_selected")
    ids = topology_geometory.intListAttribValue("ids")
    prev_processes = [_get_prev_process(topology_node, id) for id in ids]
    if prev_selected is None or len(prev_selected)==1 or len(ids)==1:
        _delete_not_necessary_param()
    else:
        if (len(prev_selected)==0):
            _delete_not_necessary_param()
            _create_param(node, ids, [_get_subids(topology_node, id) for id, prev in zip(ids, prev_processes) if prev != -1]) 
        else:
            _delete_not_necessary_param()
            _create_param(node, prev_selected, [_get_subids(topology_node, id) for id, prev in zip(prev_selected, prev_processes) if prev != -1]) 
else:
    _delete_not_necessary_param()
```

### パラメータの値を読み出す
パラメータの読み出しはhda内のpythonノードで行っています。処理としては下記のとおりです。こちらもparmTemplateGroupを元に上記で作成したパラメータを読み出しています。読みだした値をdetail attributeとして書き出すことで、インタラクティブに形状生成をできるようにしています。
```python
import itertools
node = hou.parent()

current_node = hou.pwd()
geo = current_node.geometry()

## ref https://www.sidefx.com/ja/docs/houdini/hom/locations.html
def strip_to_tokens(parm):
    bitfield = parm.eval()
    tokens = parm.parmTemplate().menuItems()
    labels = parm.parmTemplate().menuLabels()
    return ([int(token) for n, token in enumerate(tokens) if bitfield & (1 << n)], [label for n, label in enumerate(labels) if bitfield & (1 << n)])

def is_n_selected(bitfield, n):
    # bitfieldのn番目の位置(0から始まります)が"オン"ならTrueを返します。

    return bitfield & (1 << n)

def bitfield_to_list(bitfield, size=512):
    # bitfieldを受け取り、そのbitfieldの各位置が"オン"なのか"オフ"なのかを示した
    # ブール(TrueまたはFalse)のリストを返します。
    # ("size"は、ストリップ内のボタンの数です。)

    return [bitfield & (1 << n) for n in range(size)]
    
# Add code to modify contents of geo.
# Use drop down menu to select examples.
parm_template_group=node.parmTemplateGroup()
parm = parm_template_group.find("ids")
token, labels = strip_to_tokens(node.parm("ids"))
unique_sub_ids = set([int(label.split(":")[0]) for label in labels])
if 1 < len(unique_sub_ids):
    raise hou.InvalidInput("sub id must be same")
if len(unique_sub_ids)==0:
    raise hou.InvalidInput("id is not selected")

neighbour_ids=[]
for id in token:
    prims = geo.prims()
    for prim in prims:
        if id==prim.attribValue("id"):
            ids = prim.attribValue("neighbour_ids")
            neighbour_ids+=ids
            break
            
unique_neighbour_ids = set(neighbour_ids)
if 1 < len(token):
    for id in token:
        if id not in unique_neighbour_ids:
            raise hou.InvalidInput("selected id is not neighbour")
            
geo.addAttrib(hou.attribType.Global, "_selected", list(token))
```

## 3. 最後に
 説明がかなり駆け足になってしまいましたが、Houdiniで社寺建築HDAについて解説しました。少しでも参考になれば幸いです。
今回もHoudiniは自分の要求を満たしてくれた、素晴らしいツールでした！それでは、よいお年を！


明日は @minami110 さんの記事です、おたのしみに！