Houdini を Python 3 でやる!!! Part.2

# はじめに

ずいぶん前の Advent Calendar で「[Houdini を Python3 でやる!!!](https://qiita.com/takavfx/items/a019427a1062b41fc8d6)」という記事を書いた。
この時は、様々な DCC ツール（良い意味で Blender を省く）が Python 3 に移行をしている段階で、Houdini もその検証段階にあった。そのため、内容はかなり壊滅的で、結論としては「今はまだ使えないかなぁ」という結論に至っていた。
しかしながら、あれから時も経ち、Houdini のバージョンも 18.5 -> 19.0 -> 19.5 へと経てきた。実際に、現時点で僕は Houdini に関するものはもちろん Python 3 で書いているし、特に問題ないと感じている。
ということで、あの記事だしっぱで名誉挽回の機会も与えないのは酷だと思うので（？）、ちゃんと使えますよ！という記事を書こうと思った次第。
ついでに、Python 3 の最新を使っていく事で何が嬉しいかをちゃんとサンプルを元に残しておこうと思う。

## 実行環境

* Houdini 19.5.403
* Windows 11

# Houdini の Python 3 対応の経緯と将来予測

### 経緯

実際のサンプルに移る前に、Houdini Python 3 の対応経緯をまとめておこうと思う（記憶が正しければ）。

|バージョン |Python 3 バージョン  |備考                                  |
|:-------- |:-------------------|:-------------------------------------|
|18.0.x    |3.7.x               |Preview 版                            |
|18.5.x    |3.7.x               |Preview 版                            |
|19.0.x    |3.7.x               |Python 3 が標準に                      |
|19.5.x    |3.9.x               |Python 2 版の提供を廃止。3.7 の提供あり。|

### 将来予測

[VFX Reference Platform](https://vfxplatform.com) によると、**CY2023 での Python 3 バージョンは 3.10.x** との指定がある。Houdini も後続のバージョンで、こちらを対応するだろう。
また、ここのところ毎年ペースでマイナーバージョンのアップデートがあるので、再来年は[つい先日リリースされた 3.11.x](https://www.python.org/downloads/release/python-3110/) にアップデートされる可能性がある。
ものによってはライブラリ側のアップデートがなお現在進行形で行われているものも Python 3 の便利・標準機能としてかかわってくるものもあるので、ある程度追っておいた方が良いかもしれない。

ちなみに、個人的に気になっている機能としては、Python 3.11 での tomllib の標準サポート。
setuptools や、外部ツールだと AWS SAM などでも toml ファイルを .ini コンフィグパーサー(configparser モジュール) の代替として使うという事が標準になりつつあるので、この対応は気になった次第。

# Python 3 を使ったサンプル

Houdini 自体というよりも、どちらかというよりかは Python 3 の解説になってはしまうが、なるべく Houdini の構造や特性に寄り添った形にしてみたい。

## Pathlib + Enum

もはや、パイプライン作る人にとってはこれのためだけでも Python 3 使うべし！！というレベルのバッテリーインクルードモジュール。

```python:Python 2
import hip
import os

# Windows OS から渡ってきたパスはすべて replace を通す。
# これを忘れると後続の編集でバグる。
project_root = os.getenv('PROJECT_ROOT').replace(
                                        os.sep, '/') # P:\project -> P:/project
asset_root = project_root + '/assets/'

ASSET_TYPES = ('anm', 'chr', 'env', 'fx', 'rig', 'ui')

# 不正な場合、正しく Traceback エラーを起こす様にするために、エラーを定義。
class AssetTypeErorr(Exception):
    def __init__(self, message):
        self.message = message

# 型指定がないので、asset_name は文字列だとしても、
# asset_type に何を入れればいいかすぐには分からない。
def create_asset_file(asset_name, assset_type):
    asset_types = ()
    if not asset_type in ASSET_TYPES:
        # 不正な場合、正しく Traceback エラーを起こす様にする。
        raise AssetTypeErorr(
            "アセットタイプが、 {} のいずれかに当てはまりません。".format(ASSET_TYPES))

    asset_dir = asset_root + '/' + asset_type + '/' + asset_name + '/' + 'houdini'
    if not os.path.exists(asset_dir):
        os.mkdir(asset_dir)
    asset_path = ''joinpath(asset_name)
    return asset_path

def __main__ == "__main__":
    asset_name = 'chr_testpig_00'
    # asset_type に何を入れればいいのかわからないので、
    # このスクリプトのドキュメントを参照する必要がある。
    print create_asset_file(eff_testpig_00, ASSET_TYPES[2])

# 実行結果:
# P:/project_a/assets/chr/chr_testpig_00/houdini/chr_testpig_00.hip
```

```python:Python 3
import os
from pathlib import Path
from enum import Enum

project_root = Path(os.getenv('PROJECT_ROOT')) # WindowsPath('P:/project_a')
asset_root = project_root.joinpath('assets')

class AssetType(Enum):
    ANIMATION = 'anm'
    CHARACTER = 'chr'
    EFFECT = 'fx'
    EVNRIONMENT = 'env'
    RIG = 'rig'
    UI = 'ui'

def create_asset_file(asset_name: str, assset_type: AssetType) -> Path:
    asset_dir = asset_root / asset_type / asset_name / 'houdini'
    asset_dir.mkdir(parent=True, exist_ok=True)
    asset_path = asset_dir.joinpath(asset_name)
    return asset_path

def __main__ == "__main__":
    asset_name = 'chr_testpig_00'
    # 以下を入力してる時に、Python の予測変換が効くので、asset_type に何を入力
    # すべきかが分からなくても、AssetType から候補が出されてくるので迷わないし、
    # タイポも避けられる。
    # 万が一入力ミスがあっても、「AssetType のアトリビュートにそんなものは定義されていない」
    # という警告を出してくれるので、Python フレンドリーに開発・テストが出来る。
    print(create_asset_file(eff_testpig_00, AssetType.CHARACTER))

# 実行結果:
# WindowsPath('P:/project_a/assets/chr/chr_testpig_00/houdini/chr_testpig_00.hip')
```

このコードを実際に VSCode などでインテリセンスを有効にした自動補完の機能を使いながら書いてみるとこの有効さと安全さがとても実感できる。
キモは、簡潔に型安全を実現できるし、オブジェクト指向（取り扱っている情報の責任範囲が明確）だし、かつ、マルチプラットフォームに暗示的に対応してくれるところ。

:::note info
Windows におけるパス解決は非常に厄介。Python プロセス内での処理でうまく行ったと思ったら、そのパスを Windows 環境として渡したりすると `\\` となっていたものが `\\\\` と増えたりする。軽く発狂するレベルの経験があるので、Python 2 の時は replace して Posix パスライクに必ず置き換えるようにしてたりした。すると、自然と replace オンパレードになるという…、ゲシュタルト崩壊。
:::

:::note info
実際には、これに関数ドキュメントを記載したりもするが、そちらのドキュメントを生成する際にも、型指定を行っていると自動補完してくれる。
:::

:::note info
Python 2 にも、似たような挙動を求める事もできるが…。。もはや非サポート化になったものを今更選択する意義がないにしても、結局はサードパーティに頼らざるを得ないという欠点がある。（中にはセキュリティ的にアウトなモジュールもあった。）
:::

::: note warn
Enum は型指定には便利だが、**順序**を管理する上ではまだエラーが起きる事がある。あくまで識別型として扱うのが現状は無難か。
:::

## Yaml + Dataclass を使った Houdini ノード生成

Python 3 ので追加された便利機能の一つに Dataclass がある。これがとんでもなく便利なので、これは皆さん是非使った方がいい！！
軽く解説を入れると、ハッシュで扱っているものをクラスオブジェクトとして扱える様にするものといった感じ。

例えば、次の様な Yaml ファイルを用意する。
`geos` でジオメトリネットワークを定義し、`camera` でカメラを定義している。
このコンフィグで、定義されたノード群を生成してみる。

```yaml:geo_create.yaml
geos:
  - name: PIGBOSS
    nodes:
      - node_type: testgeometry_pighead
        xform:
          tx: -5
          ty: 3
          tz: -1.5 
          rx: 18
          ry: 66.3
          uniformscale: 3
  - name: LABBERTOY
    nodes:
      - node_type: testgeometry_rubbertoy
        xform:
          rx: -10.0
          ry: -100.0
          uniformscale: 0.7
camera:
  xform:
    tx: 4.04462
    ty: 0.2965
    tz: 6.57223
    rx: 10.4605
    ry: 40.5044
    rz: 4.62355
```

Python 側の実装は以下の様な感じ。
**yaml** と **dataclasses_json** は外部モジュールなので、`PYTHONPATH` 環境変数などで別途インストールする。

:::note info
Houdini の環境変数については、以前の Advent Calendar で書いた「[Houdiniと環境変数の魔導書 〜補助魔法導入で失敗しないために〜](https://qiita.com/takavfx/items/b58cae65984b15e5ad29)」を参照されたし。
:::

```python
import hou
from dataclasses import dataclass, field, asdict
from pathlib import Path

# 以下二つは外部モジュールのため、別途インストールする
import yaml
from dataclasses_json import dataclass_json

@dataclass
class Xform:
    tx: float = 0.0
    ty: float = 0.0
    tz: float = 0.0
    rx: float = 0.0
    ry: float = 0.0
    rz: float = 0.0

@dataclass
class GeoXform(Xform):
    uniformscale: float = 1.0

    
@dataclass
class GeometryNode:
    node_type: str
    name: str = field(default_factory=str)
    xform: GeoXform = field(default_factory=GeoXform)

    def create(self, root: hou.Node) -> None:
        name = self.name
        if not self.name:
            name = self.node_type
        node = root.createNode(node_type_name=self.node_type,
                               node_name=name)
        node.setParms(asdict(self.xform))


@dataclass
class GeometryNetwork:
    name: str
    nodes: list[GeometryNode] = field(default_factory=list)


@dataclass
class CameraNode:
    xform: Xform = field(default_factory=Xform)

    def create(self, root: hou.Node) -> None:
        cam = root.createNode('cam')
        cam.setParms(asdict(self.xform))


@dataclass_json
@dataclass
class ObjectNetwork:
    geos: list[GeometryNetwork] = field(default_factory=list)
    camera: CameraNode = field(default_factory=CameraNode)

    def struct(self) -> None:
        obj_root = hou.node('/obj')
        for geo in self.geos:
            geo_root = obj_root.createNode('geo', node_name=geo.name)
            for node in geo.nodes:
                node.create(geo_root)

        if self.camera:
            self.camera.create(obj_root)
        
        obj_root.layoutChildren()


def load_yaml(filename: Path) -> dict[str, any]:
    with open(filename, 'r', encoding='utf-8') as f:
        data = yaml.load(f, yaml.FullLoader)
    return data


def load_network_config(filename: Path) -> ObjectNetwork:
    return ObjectNetwork.from_dict(load_yaml(filename))

network = load_network_config(Path('geo_create.yaml')) # ファイルのパスは正しく設定してネ
network.struct()
```

Python Source Editor に張り付けて、実行するとこんな感じのシーンが出来上がる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/9a97fa1c-fd7a-5c93-4bc6-8a0d6c78be08.png)


ちょっと、無理やりにでもこういう書き方できるという事をしたので「え、有用性？」ってのは思うところがあるけれども、Houdini のネットワーク構造に慣れ親しんでいる人であれば、これがどういう事かイメージしやすいと思う。

実際には、この仕組みはツールのコンフィグで使う事が多く、コンフィグのデータからの処理実装などがしやすくなるのでとてもいい。


# まとめ

ちょっと強引な紹介も含めたけど（笑）、こうして Python 3 での実行もまったく問題なく使える様になっている。
では、よい Houdini + Python 3 ライフを！！！