Houdini18のGoZ処理の仕組みを追う

この記事は Houdini Advent calender 2019 21日目の記事です。
_さて、実は カレンダーの募集がかかった際、Houdini Apperentice 側のカレンダーにエントリーしたつもりでいたんですが、無印の方に間違ってエントリーしていたことが判明しました。しかしApperentice側ももう空きが無いようですし、巨匠に囲まれていて恐縮なのですがせっかくなので深堀りの調査系記事で勤めを果たそうと思います。_

肝心のネタですが、あまり関連記事を見かけない気がするので、HoudiniとZBrushとの連携を司るGoZについて挙動をくわしく追ってみます。（GoZはHoudini以外にもPhotoShopやMayaなどいろいろなDCCツールとZBrushを連携させるツールの総称です。）HoudiniとZBrushは正反対？の性格なソフトにも感じますが、連携は強力な機能だと思うので詳細を知りたく。

以下、Windows 10 の Houdini Indie 18.0.287 + SideFX labs Release 318 + ZBrush2020 で調査した内容です。Labsもかなり進化が早いので、すぐ細かい部分は変わってしまうかとは思います。

#　目次

* 基本的な使い方（知ってる方はスキップ！）
* 動作の仕組み
* インポート・エクスポート時に相互に変換される内容について
* テクスチャ(カラー)編集について
* サブディバイドをかけるタイミングについて
* 関連動画
* MacでGoZを動かしたい
* 感想など


# 基本的な使い方

## インストール・準備

* Houdini側は、SideFX labs (もしくは旧GameDevtools) に Houdini用のGoZが付いています。シェルフにGoZボタンがあります。一応利用前に最新にアップデートするといいでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/9eb04093-fd5f-c61e-2fc4-10f43448975b.png)

* ZBrushからHoudiniを認識するためには、このシェルフのStart GoZボタンでGoZを有効化してからでないとダメなようです。
* ZBrush側では最初にHoudiniのありかを選択しないといけません。
*
* core/fxなどHoudiniの種類の選択肢が出ますが、Indieの場合は通常のHoudiniを選択すれば大丈夫でした。
* 当然ですが、Houdiniを立ち上げるたびにシェルフのボタンを押して接続準備をしてあげなくてはいけません。
* ZBrushでは他にもシステムにインストールしている対応アプリケーション（PhotoShop/Maya/Cinema4Dなど）も認識します。
* 一度にGoZでZBrushと連携が取れるのは１つのアプリだけなので、切り替える場合はリセットボタンを押します。
* 関連動画リンクがこのページの最後にあります。

## ZBrushからHoudiniへのメッシュインポート

ZBrush側の操作はツールパネルから行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/81c9ee67-8e64-5e39-1687-269b051ca115.png)

ZBrush側からGoZボタン(もしくは全てボタン/表示のみボタン)を押せばHoudini側にすぐインポートされる、、ようなんですが、手元ではどうもうまくいかず（空のSOPネットワークは作られるがメッシュが引き渡されて来ない。）`GoZ_import` SOP を作り、ZBrushからエクスポート後、リロードボタンを押してやることでメッシュの読み込みができました。自分の使い方がおかしいのかもですが、そもそも仕様が変わったのかもしれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/adfea39f-f0ff-2b0a-28ad-195dfa6cf69f.png)
ZBrushのサンプル犬をHoudiniに読み込んだ様子。

ZBrush側でサブディビジョンをかけてる場合、一番低レベルの物しかエクスポートされないようなので必要であれば低レベルの物を削除します。ジオメトリ以外では、ポリペイントと、ポリグループ情報とマスクがエクスポートできました。（マスクをやりとりに使うか若干疑問ですが、通常グループと異なるグルーピングがやりとりできるので便利なのかもしれません。）サブツールもグループに変換はされませんが、アトリビュートで区別できる状態で持っていけます。

それぞれ、どこの何アトリビュートに変換されるかは後述します。

## HoudiniからZBrushへのメッシュエクスポート

`GoZ_export` SOP を作り、エクスポートしたいメッシュを入力に繋ぎ、`Send to ZBrush` ボタンを押します。すぐさまZBrushに取り込まれます。
この時SOPの名前がZBrushのサブツール(メッシュ)名になるという情報をみましたが、自分の場合はそうなりませんでした。代わりにプリミティブにnameアトリビュートがある場合、それがサブツール名として使われました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/b72104dc-2136-aa64-6a63-d095babf40c3.png)
rubbertoyをエクスポートした様子。

ジオメトリ以外ではポイントのカラー（頂点ではない）がエクスポートできました。またグループはエクスポートできません。（が、ほかの方法でグループ的なエクスポートはきます。一度 wrangle をかますと良さそうです。）こちらも詳細は後述します。UV情報もエクスポートできましたが、テクスチャーは自動で設定されませんでした。


# 動作の仕組み

ここからもうちょっと突っ込んで挙動を追います。両ソフトお互いに、エクスポートを行うと、ファイルシステムにメッシュデータが書き出され、相手のソフトに読み込みを促します。拡張子`GoZ`のバイナリファイルが中間データとして使われていました。
このファイル形式の詳細はわかりませんが、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/5fdde714-0631-671a-674e-6edb2a6de45d.png)
File SOPで読み込む事が可能でした。独自のバイナリ形式をインポートできるようにするための仕組みがあるのですかね。

関連ファイルは以下の場所に置かれています。

 * `C:\Users\Public\Pixologic\GoZProjects\Default`
    * 一時ファイル置き場です。Houdini以外とのGoZ連携時もここが使われるようです。
 * `C:\Users\Public\Pixologic\GoZBrush`
    * ZBrushが連携するアプリケーション(今回はHoudini)からメッシュをエキスポ―とする際などに呼び出すプログラム(exe)と、そのための情報を保持するテキストファイルがいくつかあります。
 * `C:\Users\Public\Pixologic\GoZBrush\Scripts`
    * GoZのためのZBrushプラグインファイル(zsc)がたくさんあります。バイナリらしく詳細不明です（が、ファイル名で何をしてるかはなんとなく予想が）。
 * `C:\Users\Public\Pixologic\GoZApps\Houdini`
    * Houdiniと連携させるための設定テキストファイルとpythonファイルが配置されています。

## ZBrushからHoudiniへのメッシュインポート(詳細)

GoZApps\Houdiniには `GoZBrushToHoudini.py` というpythonファイルがあり、これがZBrushから呼び出され、Houdiniを操作するようです。
hrpycモジュールを用い、RPyCというプロトコルでHoudini外からHoudiniを操作します。

hrpyc モジュールを知りませんでしたが、このように外部ソフトと連携させる際に便利そうです。
参考 hrpycモジュール https://ikrima.github.io/houdini_additional_python_docs/hrpyc.html

150行程度なので内容も難しくないです。なんですが、このプログラムが自分の手元で中途半端にしかうまく動いていない感じで、いろいろ記事に間に合わせようとしていましたが、調査中となってしまいました。（別記事にまとめられたらいいなと思います。）
前述の通りGoZ_import SOPを用いて Houdiniへメッシュの読み込みをしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/5bb8f01f-bf44-1450-f06e-ed4df9a4c353.png)

GoZ_importの中身は意外とシンプルで、指定されたファイルの数だけ読み込みを繰り返して出力としているだけでした。

## HoudiniからZBrushへのメッシュエクスポート(詳細)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/ce3ea479-0e91-887e-c580-21d2045d3d1c.png)

ネットワークはこのようになっており、さして複雑ではないです。エクスポート関連の処理は行うものの、Houdini内でのメッシュ編集は行っていないので、インプットがそのままアウトプットになっています。エクスポート前にnゴン(3角、4角以外のポリゴン)を分割する処理が入ります、それとnameアトリビュートがない場合、名前づけの処理が入ります。最後にGoZファイルを書き出します。

主だった処理は HDAの Python script 設定に記述されています。下記、ソースそのまま張りますが、日本語コメントは自分が書いたものです。

```python
import os
import subprocess

# 書き出し場所やキックする実行ファイルのパスの設定
zbrush_dir = "C:\Users\Public\Pixologic\GoZBrush"
zbrush_proj_dir = "C:\Users\Public\Pixologic\GoZProjects\Default"

# 作成するテキスト系ファイルのパス一覧
ascii_file_map = {
            "GoZ_Info.txt":"C:/Users/Public/Pixologic/GoZApps/Houdini/GoZ_Info.txt",
            "GoZBrushToHoudini.py":"C:/Users/Public/Pixologic/GoZApps/Houdini/GoZBrushToHoudini.py",
            "GoZEditHoudini.bat":"C:/Users/Public/Pixologic/GoZApps/Houdini/GoZEditHoudini.bat",
            "GoZLocateHoudini.bat":"C:/Users/Public/Pixologic/GoZApps/Houdini/GoZLocateHoudini.bat",
}

# prim のnameアトリビュートから重複を取りのぞいて返す
def get_unique_names_from_node(node):
    export_geo = node.geometry()
    names = export_geo.primStringAttribValues("name")
    unique_names = list(set(names))
    
    return unique_names

# meshのパスを解決する
# GoZ_ObjectPath.txtファイルを引数と戻り値の格納につかっている、パスの解決はGoZMakeObjectPath.exeファイルにまかせている
def get_mesh_paths(meshes_to_send):
    paths_to_meshes = []
    for mesh in meshes_to_send:
        object_path_file = os.path.join(zbrush_dir, "GoZ_ObjectPath.txt")
        f = open(object_path_file, 'w')
        f.write(mesh + "\n")
        f.close()

        object_path_exe = os.path.join(zbrush_dir, "GoZMakeObjectPath.exe")
        subprocess.call(object_path_exe)

        object_path_file = os.path.join(zbrush_dir, "GoZ_ObjectPath.txt")
        f = open(object_path_file, 'r')
        for line in f.readlines():
            print line
            paths_to_meshes.append(line)

        f.close()

    return paths_to_meshes

# GoZ_ObjectList.txtにパス情報を書き出す
def write_object_path(path_to_meshes):
    object_path_file = os.path.join(zbrush_dir, "GoZ_ObjectList.txt")
    f = open(object_path_file, 'w')
    for path in path_to_meshes:
        f.write(path.replace("\\", "/") + "\n")
    f.close()

# サブツール名をノードのパラメータから取得して、ノードグラフでGOZファイルを書き出させる
def export_subtool(instance):
    meshes_to_send = [instance.parm("tool_name").eval()]
    
    paths_to_meshes = os.path.join(zbrush_proj_dir, meshes_to_send[0])
    if instance.inputs():
        instance.node("OUT_MESH").parm("sopoutput").set(paths_to_meshes+ ".GoZ")
        instance.node("OUT_MESH").parm("execute").pressButton()

# 実際の書き出し処理メイン
# Send to ZBrush ボタンを押した際に呼ばれる
# ノードグラフの動作に必要な各種パラメータの設定を行う
# ノードグラフを実行後（ファイル書き出し後）、ファイルをZBrushに読み込むための実行ファイルを叩く
def export(kwargs):
    node = kwargs["node"]
    meshes_to_send = [node.parm("tool_name").eval()]
    
    export_node = node.node("EXPORT_NODE")
    meshes_to_send = get_unique_names_from_node(export_node)

    blast_node = node.node("isolate_part")
    rop_node = node.node("OUT_MESH") 
    
    #export_path = hou.pwd().node("OUT_MESH").parm("sopoutput").eval()
    
    paths_to_meshes = [os.path.join(zbrush_proj_dir, mesh_name).replace("\\", "/") for mesh_name in meshes_to_send]
    
    write_object_path(paths_to_meshes)
    if hou.pwd().inputs():
        for name in meshes_to_send:
            node.parm("__name_expression").set("@name=" + name)
            node.parm("__export_file").set(zbrush_proj_dir + "\\" + name + ".GoZ")
            rop_node.parm("execute").pressButton()
    
    object_path_exe = os.path.join(zbrush_dir, "GoZBrushFromApp.exe")
    subprocess.call(object_path_exe)   
    
# プラグイン動作に必要なファイルを作成する
def saveSectionToFile(section, file_name, mode="w"):
    '''Given a section, save it to a file.'''
    try:
        section_file = open(file_name, mode)    
        section_file.write(section.contents())
        section_file.close()
        return True
    except Exception as e:
        print e
        return False 

# Houdini用のGoZをZBrushにインストールする
def install(node):

    definition = node.type().definition()
    sections = definition.sections() 
    errors = False 

    if not os.path.exists(zbrush_dir):
        return 
    
    for ascii_file in ascii_file_map:
        file_path = ascii_file_map[ascii_file]
        dirname = os.path.dirname(file_path)
        if not os.path.exists(dirname):
            os.makedirs(dirname)
        success = saveSectionToFile(sections[ascii_file], file_path)

# 外部から動作処理をうけつけるサーバーを起動する
# シェルフに記載されているものと同じ処理
def start_server():
    import hrpyc 
    already_running = True
    
    try:
        connection = hrpyc.rpyc.classic.connect("127.0.0.1", 18811)
    except:
        already_running = False 
            
    if not already_running:
        hrpyc.start_server()
```

## インポート・エクスポート時に相互に変換される内容について

下記のような割り当てになっていました。ダイレクトにHoudiniのグループが変換されないので、エクスポート・インポート時にグループを下記アトリビュートに変換・復帰するvexなど用意するといいかもしれません。

| 項目 | Houdini | ZBrush | 備考 |
|---|---|---|---|
|ポリゴン形状  |Nゴンを許可  |三角と四角のみ  | NゴンはHoudiniからエクスポート時に分割|
|グルーピング1  |Primの`name`アトリビュート |サブツール名  |アトリビュートは文字列 (※1)|
|グルーピング2  |Primの`polygroup`アトリビュート |ポリグループ  |アトリビュートは整数値 マイナスの値も持つ (※2)|
|グルーピング3  |Pointの`mask`アトリビュート |マスク状態  |アトリビュートは 0.0 ~ 1.0 のfloat値 マスクは中間値を持つ (※3)|
|カラー |Pointの`Cd`アトリビュート | ポリペイント| いわゆる頂点カラー|
|UV |Vertの`uv`アトリビュート| UV値| テクスチャ情報はやりとりなし|

* ※1 サブツールはZBrushのメッシュのレイヤー構造のようなものです。
* ※2 ポリグループはZBrushのグルーピングで、１ポリゴンは複数のポリグループには属せません。
* ※3 マスクはそのままではHoudini上で可視化されません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/59c4be55-693b-4be9-6338-73dee1bdbc6f.png)
この状態でZBrushからエクスポートすると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/b9b59866-6030-9357-16ea-67e9d2698e1b.png)
このようにHoudiniに読み込まれ、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/9707500b-f9e6-ead4-3544-5eff79d39346.png)
ポイントアトリビュートはこのように（Cdとmaskがある、マスクは小数値を持つ)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/d727af6c-6fb9-5ab5-229b-1858df6f98a2.png)
頂点アトリビュートはこのように（uv情報がある)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8302/5d882fa0-d3df-eac5-f7df-8b4233518f80.png)
プリムアトリビュートはこのように（nameとpolygroupがある、polygroup値はマイナスの値も持つ)
それぞれなります。


## テクスチャ(カラー)編集について

使っている人は知っている事ですが、ZBrushでのカラーペイント編集(ポリペイント)は テクスチャでなく、実態は *頂点カラー* です。ハイメッシュで編集する場合、頂点カラーでの着色でも十分な細かさで着色ができます。むしろ下手に低解像度テクスチャを使うより美しくなります。（もちろんZBrush内でもUV＋テクスチャの表示自体はできますし、ポリペイントとテクスチャを相互に変換する機能はあります。）よってHoudini内でテクスチャを適用したメッシュをZBrush内でリペイントしたい場合は、一度どちらかのソフト上で頂点カラーに変更する必要があります。公式の動画(URLは後述)では、適当にテクスチャをポイントカラーに変更してから、ZBrushにエクスポートしてペイントしていました。自分がざっと試したかぎりではHoudiniで頂点にカラーを割り振るよりも、ZBrushでテクスチャを張りなおしてからポリグループに変換した方が良い見た目の結果になりましたが、後者の場合手間が増えてしまうので、適切なノード構成でHoudini側でポイントにカラーを割り振った方がいいでしょう。そうなると次の項目にあるように前もってメッシュを細かくしておいた方がよいでしょう。

なお、カラー以外のマテリアル情報はやりとりできないようです。maskアトリビュートを使ってマテリアルを変更したい部分を受け渡す事はできそうです。（合計２つしかマテリアルの分布を指定できず、かつZBrush側で手作業でマテリアルを設定しなくてはいけなくなりますが。）

## サブディバイドをかけるタイミングについて

公式ビデオでは、ZBrushにもっていってからサブディバイドをかけてスカルプトをほどこして低レベル削除してHoudiniに戻す、という手順の作業が録画されていましたが、ここについては、Houdini側であらかじめサブディバイドをかけてしまったほうがHoudini内の前後の編集に影響を与えずらい(頂点番号などが変更されない)ので好ましいように思います。まだそんなに使い込んでいないのでわかりませんが。

## 関連動画
2年前の動画で今とだいぶ状況違いますが公式ページなので張っておきます。雰囲気はつかめると思います。もっと新しい動画で良いものがあれば追って追記します。

[INSTALLING GoZ PLUGIN FOR ZBrush 2017/6](https://www.sidefx.com/tutorials/installing-GoZ-plugin-for-ZBrush/)
* 上部にインストールのやり方をおさめた動画あり
* 株にカラー情報付きメッシュをエクスポートしてインポートしている動画あり

## MacでGoZを動かしたい
自分は普段Macでプログラムもクリエイティブ制作も行うのですが、NVidiaのGPUがMacに積まれる見通しが全くないため、Windowsマシンも別途購入しました。MacでHoudiniを使っている際は、ずっとGoZがWindowsしか対応していない事がとても残念だったのでここはなんとかしたく。。
実はこのポストは当初、MacでもがんばったらGoZが動いたよ、的なサプライズな内容にしたかったのですが、このように調べてみると、サクッと対応はできなさそうだなあという印象を持ちました。まだ調査中ではあるので、Mac側でMayaなどのソフトとのGoZの挙動も行いつつ、どうにかできないかは引き続き考察してみたいです。しかし、HoudiniユーザーはほとんどがWindowsユーザーで、ついでLinux、Macはほぼいないという現状だという事なので、もし実現したところで需要は少なそうですね。

## 感想など
HoudiniとZBrushと連携させてかつサブスタンスプラグインを連結するとモデリング環境としてかなりの強い状態だと思っているのですが、最近勢いのすごいBlenderもスカルプト機能が今後かなり強化されていくようで、そちらも気になります。素の状態でZBrush+Mayaのような状態になるわけなので。あと、ZBrushのプラグインを作ってる人はあまり見かけない印象ですが、今回GoZを解析してみて可能性は感じました。

皆が期待しているいけてるテクニック系の投稿でなく恐縮ですが、以上です。