アトラステクスチャから軽量なポリゴンを生成するツールの作成

アトラステクスチャからカードポリゴン生成、描画に優しい形状へ変換、そしてHoudini 17で追加されたPython Viewer Statesを使用したPivot設定に関して解説します。

まずはどんなツールなのかを録画しましたのでご確認ください。
https://youtu.be/gcBinxr6f1I


# アトラステクスチャからポリゴンへの流れ
1．テクスチャを読み込む
2．テクスチャの境界を調整
3．テクスチャをトレースしてポリゴンへ変換
4．生成されたポリゴンを矩形化
5．UVを生成
6．Pivotを使用して位置を調整


## テクスチャを読み込み、テクスチャの境界を調整
テクスチャの読み込みはCOPを使用します。テクスチャの種類はOpacityだけでいいのですが、表示用にAlbedoも使用しています。ここでポイントになるのは DilateErodeノードを使用した輪郭の調整です。コントラスト等でもある程度調整可能ですが、例えば枯れた木の葉などでは小さな穴が開いている事もありますので、DilateErodeノードであれば小さな穴を埋める事ができます。
![TEX2CARD_TEX.png](https://qiita-image-store.s3.amazonaws.com/0/144656/c2c12f2b-7af1-dd66-0164-0d2720645cc5.png)

## テクスチャをトレースしてポリゴンへ変換
テクスチャからポリゴンへの変換はTrace SOPで行います。またCOPから画像を参照するには op:パス名 を使用します。
```op:`opfullpath("../image/OUT_OPAC")`
```
またTrace SOPで生成されたポリゴンの頂点数、頂点間隔の調整はカーブの調整でお馴染みのResample SOPを使用することでエッジの頂点を調整することができます。
![TEX2CARD_TRACE.png](https://qiita-image-store.s3.amazonaws.com/0/144656/f7b5a64a-d091-6d8f-5160-0cfe2c070424.png)

## 生成されたポリゴンを矩形化
テクスチャからポリゴン化したものをさらに矩形へ変換します。矩形への変換はforeachでPrimitive分ループして、矩形(Grid)をmatchsizeで各Primitiveの大きさにマッチさせていきます。これで矩形化は完了です。
![TEX2CARD_CARD.png](https://qiita-image-store.s3.amazonaws.com/0/144656/40b732da-1e56-8166-fdb5-ae68cec547d7.png)
ここからもうひと手間加えて、描画に優しい形状へ変換していきたいと思います。ここで言う描画に優しいとは？透明部分をできるだけ排除した形状を指します。当初Python SOPを使用して形状を変換していましたが、convexhullを2D化すればいいのでは？と思い、検証した結果、Shrinkwrap SOP, traiangulate2d SOPで実現できました。さらにPolyreduceとTrace後にエッジのresampleの組み合わせで軽量なデータへ変換できます。
![TEX2CARD_OD.JPG](https://qiita-image-store.s3.amazonaws.com/0/144656/e01522a8-2722-a0f3-0a5a-1a6cb4d39b0f.jpeg)

## UVを生成
UVの生成は頂点位置がそのままUVに対応しているので、Wrangleで ```v@uv = @P;``` で完成です。
![TEX2CARD_UV.png](https://qiita-image-store.s3.amazonaws.com/0/144656/8b67582e-300b-cbb1-895e-3e8ffccbbaff.png)


## Pivotを使用して位置を調整
Pivotは各矩形の中心を求めてPivotとし、Pivotが[0,0,0]になるように頂点を移動しています。またHoudini 17で搭載された新機能のPython Viewer Statesを使用して形状上の任意の位置にPivotを設定できる機能を実装しました。

### Python Viewer States
Python Viewer Statesはビューポートのインタラクションを制御できる機能です。例えば回転ツールはViewer Statesの一つで、このような機能をPythonを使用して独自に実装できるのがPython Viewer Statesという事のようです。
※現在Python Viewer StatesはHDAからしか利用できません。
https://www.sidefx.com/docs/houdini/hom/python_states.html

### Python Viewer Statesを使用してPivotツールを実装
Pivotツールの実装はHELPに記載されています、[DrawPoints](https://www.sidefx.com/docs/houdini/hom/state_node.html#draw_points)をほぼそのまま使用しています。
実装の概要は、
- Viewer Statesの動作を実装した DrawPointsクラス
- 登録に必要なViewerStateTemplateの生成
で構成されています。
これらの実装はHDAのScripts/Python Moduleへ設定しています。
![TEX2CARD_SCRIPT.png](https://qiita-image-store.s3.amazonaws.com/0/144656/4c73c29e-ae22-cbba-5e9c-64503d4fa4f8.png)


### DrawPointsクラス
onMouseEventが発生すると、生成したポリゴンへ例を飛ばして、ヒットしたらその位置をAdd SOPへ追加する


```python
from __future__ import print_function

import stateutils

class DrawPoints(object):    
    def __init__(self, state_name, scene_viewer):
        self.state_name = state_name
        self.scene_viewer = scene_viewer

        self._node = None
        self._pressed = False
        self._index = 0    

    def _point_count(self):
        multiparm = self._node.parm("points")
        # This is how you get the number of instances in a multiparm
        return multiparm.evalAsInt()

    def _insert_point(self):
        index = self._point_count()
        multiparm = self._node.parm("points")
        multiparm.insertMultiParmInstance(index)
        return index

    def _set(self, index, position):
        self._node.parm("usept%d" % index).set(1)
        self._node.parmTuple("pt%d" % index).set(position)

    def _start(self):
        if not self._pressed:
            self.scene_viewer.beginStateUndo("Add point")
            self._index = self._insert_point()
        self._pressed = True

    def _finish(self):
        if self._pressed:
            self.scene_viewer.endStateUndo()
        self._pressed = False

    def onMouseEvent(self, kwargs):
        node = self._node = kwargs["node"]
        ui_event = kwargs["ui_event"]
        device = ui_event.device()
        origin, direction = ui_event.ray()

        # Find intersection with geometry or ground
        intersected = -1
        cards = node.node("OUT")
        # Only try intersecting geometry if this node has input
        if cards:
            geometry = cards.geometry()
            intersected, position, _, _ = stateutils.sopGeometryIntersection(geometry, origin, direction)
        #if intersected < 0:
        #   position = stateutils.cplaneIntersection(self.scene_viewer, origin, direction)

        # Create/move point if LMB is down
        if device.isLeftButton():
            self._start()
            self._set(self._index, position)
        else:
            self._finish()

    def onInterrupt(self, kwargs):
        self._finish()
```

### [ViewerStateTemplate](http://www.sidefx.com/docs/houdini/hom/hou/ViewerStateTemplate.html)
ViwerStateTemplateは独自のViewerStatesを作成するためのテンプレートになるオブジェクトです。

```python

# Grab a reference to the asset's node type
nodetype = kwargs['type']
# Use the node's name and label as the state name and label
state_name = nodetype.name() + ".pystate"
label = nodetype.description()
category = nodetype.category()
# Instantiate ViewerStateTemplate with the state name, label,
# and node type category
template = hou.ViewerStateTemplate(state_name, label, category)

# Mandatory binding
template.bindFactory(DrawPoints)
# Other optional bindings would go here... 1
```
### Viewer Statesの登録と解放
ViewerStatesの登録、解放はPythonで実装する必要があります。それぞれ実装先は
登録はOnInstall
![TEX2CARD_ONINSTALL.png](https://qiita-image-store.s3.amazonaws.com/0/144656/6ae2f01d-34ae-25e4-fb36-5eac504f2484.png)

```python
module = kwargs['type'].hdaModule()
hou.ui.registerViewerState(module.template)
```

解放はOnUninstall
![TEX2CARD_ONUNINSTALL.png](https://qiita-image-store.s3.amazonaws.com/0/144656/6b733aa6-1fa5-e763-41c1-94dec33723b1.png)

```python
state_name = kwargs['type'].name() + ".pystate"
hou.ui.unregisterViewerState(state_name)
```


## おまけ

[ここから](https://www.dropbox.com/s/y667k26xdx3mziq/tex2card.hda?dl=0)HDAがDownloadできます。
※Houdini 17用ですので、17以前のバージョンでは動作しません。