アトリビュートが付与されたノードを一撃で特定するツールの開発

## **必要になった経緯**  
　他人からhipファイルを渡された時に複雑なノードネットワークの中でアトリビュートの起点や変更を知りたい時に手動だと大変な場合があります。ノードの右下に付与されたアトリビュート名が表示されるのでそれを目印に探すことは可能ですが、大規模ネットワークだと少々手間です。
　また、デバッグや最適化をする際にもすぐに起点、変更ポイントに行きたくても時間がかかります。
このような課題を解決するため、 **「アトリビュートが付与されたノードを一撃で特定するツール」** を開発しました。


## **アトリビュートがどこで付与されたかを探る手段**  
Houdiniの標準機能では、以下の方法で確認することができます：  

1. **手動で上流ノードを一つずつ確認**  
   - ノードのパラメータやGeometry情報をチェックします。  
   - ネットワークが大きい場合、非常に手間がかかります。  

2. **Geometry Spreadsheetを利用**  
   - アトリビュートの存在や値を直接確認できますが、どのノードで付与されたかを知るには上流ノードを順番に選択し、スプレッドシートでアトリビュートの有無や値の変化を確認する必要があります。

3. **Attribute CreateやWrangleノードを探す**  
   - アトリビュートは通常、以下のようなノードで作成されます
     - Attribute Create
     - Attribute Wrangle
     - Attribute Expression
     - VOPノード（Attribute VOP）
   - ネットワーク内でこれらのノードを探し、特定のアトリビュートが操作されている箇所を確認しますが、手動では時間がかかります。
  
4. **Node Infoを利用する**  
   - 現在のノードを選択し、右クリックしてNode Infoを開きます。
   - 情報に表示されるアトリビュート名や依存関係をチェックします。
   - こちらも手動では時間がかかります。


これらの手段には限界があるため、効率的にアトリビュート付与元を探索するツールが必要でした。



## **コードの特徴と仕組み**  
開発したツールは、選択したノードを起点に上流のノードネットワークを**再帰的に探索し、指定したアトリビュートが初めて生成されたノードを特定**するものです。  
さらに、ユーザーが入力しやすいようポップアップUIを実装し、選択状態やフォーカスの視覚的フィードバックも提供します。

---

### **コードの仕組み**

#### **1. 上流をたどってアトリビュートを探すロジック**  
```python
def find_attribute_origin(node, attrib_name, attrib_type):
    visited = set()  # 既にチェックしたノードを記録

    def traverse_upstream(current_node):
        if current_node in visited:  # ループ回避
            return None
        visited.add(current_node)

        try:
            geo = current_node.geometry()
        except hou.GeometryPermissionError:
            return None

        attrib_list = {
            "Point": geo.pointAttribs,
            "Primitive": geo.primAttribs,
            "Vertex": geo.vertexAttribs,
            "Detail": geo.globalAttribs
        }.get(attrib_type, lambda: [])()

        if any(attrib.name() == attrib_name for attrib in attrib_list):
            input_nodes = current_node.inputs()
            if not input_nodes:
                return current_node
            for input_node in input_nodes:
                origin = traverse_upstream(input_node)
                if origin:
                    return origin
            return current_node
        return None

    return traverse_upstream(node)
```

- **再帰的な探索**：`inputs()`で上流ノードを辿り、アトリビュートが存在するかチェックします。  
- **ループ回避**：`visited`セットを使用して、循環するネットワークでも無限ループにならないようにします。  
- **アトリビュートの存在チェック**：指定されたタイプ（Point, Primitive, Vertex, Detail）でアトリビュートを検索します。  

---

#### **2. ユーザー入力ポップアップUI**  
```python
result = hou.ui.readMultiInput(
    "Search Attribute Origin",
    input_labels=["Attribute Name", "Attribute Type (Point/Primitive/Vertex/Detail)"],
    buttons=("OK", "Cancel"),
    close_choice=1,
    initial_contents=["my_attribute", "Point"]
)
```

- **`readMultiInput`の使用**：ポップアップUIでアトリビュート名とタイプをユーザーが入力できます。  
- **初期値設定**：`initial_contents`でデフォルト値を提供し、入力の手間を減らします。  

---

#### **3. 結果のノード選択とフォーカス**  
```python
if origin_node:
    origin_node.setSelected(True, clear_all_selected=True)
    network_editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)
    if network_editor:
        network_editor.setCurrentNode(origin_node)
```

- **選択状態の設定**：`setSelected(True)`で該当ノードを選択状態にします。  
- **ネットワークビューのフォーカス**：`setCurrentNode()`でネットワークエディタを対象ノードにフォーカスします。  

## **今回作成したツール　Attribute Tracking**
![2024-12-18_23h21_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/134079/c630d9ce-5bfd-12aa-6260-5cc2ca3743ea.png)

```py
def find_attribute_origin(node, attrib_name, attrib_type):
    """
    現在のノードから上流をたどり、指定されたアトリビュートを初めて生成したノードを探す。
    """
    visited = set()  # 既にチェックしたノードを記録

    def traverse_upstream(current_node):
        # ノードがすでにチェック済みの場合はスキップ
        if current_node in visited:
            return None
        visited.add(current_node)

        # 現在のノードのGeometryを取得
        try:
            geo = current_node.geometry()
        except hou.GeometryPermissionError:
            return None  # Geometryが存在しない場合はスキップ

        # アトリビュートが存在するかチェック
        attrib_list = {
            "Point": geo.pointAttribs,
            "Primitive": geo.primAttribs,
            "Vertex": geo.vertexAttribs,
            "Detail": geo.globalAttribs
        }.get(attrib_type, lambda: [])()

        if any(attrib.name() == attrib_name for attrib in attrib_list):  # 指定タイプでアトリビュートが存在するか
            # このノードに到達する前に上流をさらにチェック
            input_nodes = current_node.inputs()
            if not input_nodes:  # 入力ノードが無い場合、このノードが起点
                return current_node

            # 上流ノードを再帰的にチェック
            for input_node in input_nodes:
                origin = traverse_upstream(input_node)
                if origin:
                    return origin
            return current_node  # 上流に該当ノードが無い場合、現在のノードが起点
        return None  # アトリビュートが無い場合はスキップ

    return traverse_upstream(node)


# ポップアップUIを再構築
result = hou.ui.readMultiInput(
    "Search Attribute Origin",
    input_labels=["Attribute Name", "Attribute Type (Point/Primitive/Vertex/Detail)"],
    buttons=("OK", "Cancel"),
    close_choice=1,
    initial_contents=["my_attribute", "Point"]  # アトリビュート名とタイプの既定値
)

# 入力内容を取得
if result[0] == 1:  # キャンセルが押された場合
    hou.ui.displayMessage("Operation cancelled.")
else:
    attribute_name = result[1][0]
    attribute_type = result[1][1]

    if not attribute_name or attribute_type not in ["Point", "Primitive", "Vertex", "Detail"]:
        hou.ui.displayMessage("Invalid input. Please provide valid attribute name and type.")
    else:
        # 選択ノードと探すアトリビュート名を使用
        selected_nodes = hou.selectedNodes()
        if not selected_nodes:
            hou.ui.displayMessage("No node selected. Please select a node and try again.")
        else:
            selected_node = selected_nodes[0]  # 最初の選択ノード
            origin_node = find_attribute_origin(selected_node, attribute_name, attribute_type)

            # 結果を表示し、該当ノードを選択
            if origin_node:
                # 該当ノードを選択状態に設定
                origin_node.setSelected(True, clear_all_selected=True)

                # ビューアを該当ノードにフォーカス
                network_editor = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor)  # ネットワークエディタを取得
                if network_editor:
                    network_editor.setCurrentNode(origin_node)  # ネットワークエディタを該当ノードにフォーカス

                # 結果を表示
                hou.ui.displayMessage(f"Attribute '{attribute_name}' ({attribute_type}) was first created at node: {origin_node.path()}")
            else:
                hou.ui.displayMessage(f"Attribute '{attribute_name}' ({attribute_type}) not found upstream.")
```

## **使い方**  

1. **ツールの実行**  
   Shelf Toolに登録して実行します。  

2. **ポップアップUIで入力**  
   - **Attribute Name**: 探したいアトリビュート名（デフォルト: `my_attribute`）  
   - **Attribute Type**: アトリビュートのタイプ（デフォルト: `Point`）  

3. **結果の確認**  
   - 該当のノードが自動的に選択され、ネットワークビューでフォーカスされます。  
   - 結果はダイアログにも表示され、対象ノードのパスを確認できます。  

---

## **発展**  
このツールは拡張することができ、以下の機能追加が考えられます：  

1. **アトリビュートの値変更の追跡**  
   指定したアトリビュートがどのノードで値を変更されたのかを追跡する機能です。
   これにより、初期生成だけでなく、値が変わる原因を特定できます。 
   ```python
   def find_attribute_modifications(node, attrib_name, attrib_type):
    """
    上流をたどり、指定されたアトリビュートの値が変更されたノードを特定する。
    """
    visited = set()

    def traverse_upstream(current_node, prev_values=None):
        if current_node in visited:
            return []
        visited.add(current_node)

        try:
            geo = current_node.geometry()
        except hou.GeometryPermissionError:
            return []

        # 現在のノードのアトリビュート値を取得
        attrib_list = {
            "Point": geo.pointAttribs,
            "Primitive": geo.primAttribs,
            "Vertex": geo.vertexAttribs,
            "Detail": geo.globalAttribs
        }.get(attrib_type, lambda: [])()

        target_attrib = next((a for a in attrib_list if a.name() == attrib_name), None)

        modified_nodes = []
        if target_attrib:
            current_values = geo.attribValue(attrib_name) if attrib_type == "Detail" else None
            if current_values != prev_values:
                modified_nodes.append(current_node)
            prev_values = current_values

        # 上流ノードを探索
        for input_node in current_node.inputs():
            modified_nodes.extend(traverse_upstream(input_node, prev_values))

        return modified_nodes

    return traverse_upstream(node)
    ```


2. **複数アトリビュートの同時検索**  
   複数のアトリビュート名を入力し、一度に検索する機能です。手動で何度も検索する手間を省きます。
   複数アトリビュート名をカンマで区切って入力できるようにします。
   それぞれのアトリビュート名に対してfind_attribute_origin関数を呼び出して検索します。

    ```python
    result = hou.ui.readMultiInput(
    "Search Multiple Attributes",
    input_labels=["Attribute Names (comma-separated)", "Attribute Type"],
    buttons=("OK", "Cancel"),
    close_choice=1,
    initial_contents=["attrib1,attrib2,attrib3", "Point"]
    )

    if result[0] == 0:  # OKボタンが押された場合
        attribute_names = [name.strip() for name in result[1][0].split(",")]
        attribute_type = result[1][1]
        for name in attribute_names:
            origin_node = find_attribute_origin(selected_node, name, attribute_type)
            if origin_node:
                print(f"Attribute '{name}' found at node: {origin_node.path()}")
   
    ```
   

3. **ビジュアル強化**  
   ネットワークビュー上で、アトリビュートの流れを可視化します。  
    -  選択ノードのカラーリング
       - 対象となるノードに対して、一時的に分かりやすい色（例: 赤やオレンジ）を付けて強調します。これにより、複雑なネットワークの中でも一目で目的のノードが分かります。
       - ```python
          # ノードのカラーリング
                      node.setColor(hou.Color((1.0, 0.3, 0.0)))  # 赤色に設定       
            ```
    - フラグやマーカーを追加
      - 特定のノードにアトリビュートを生成・変更した印として、バッジ（フラグ）やカスタム名を付けて目立たせます。
      - ```python
            # ノード名にマークを追加
            node.setName(node.name() + "_ATTR_SOURCE", unique_name=True)

        ```
        ノード名に「_ATTR_SOURCE」のマークが付き、生成・変更点が一目で識別できます。

4. **デバッグログの出力**  
   探索過程をログファイルに出力し、後から確認可能にする。  
   長くなりそうなので別途ページを分けて説明する予定です。

---

## **参考資料**  

[Refactoring Houdini Node Network – メンテナンス性の高いノードネットワーク構築のために](https://www.technical-artist.net/?p=1737)

[Houdini でプログラミング：アトリビュート ](https://support.borndigital.co.jp/hc/ja/articles/900003179946-Houdini-%E3%81%A7%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0-%E3%82%A2%E3%83%88%E3%83%AA%E3%83%93%E3%83%A5%E3%83%BC%E3%83%88)

[そんなことよりHoudiniやろうぜ！](https://techblog.sega.jp/entry/2018/01/25/100000?utm_source=chatgpt.com)

[Houdini Tutorial: Send debug output (VEX) to the Houdini Console](https://www.youtube.com/watch?v=MsJuqqz64Kk)


[サルにもわかる Houdini: Using Python in Houdini](https://ikatnek.blogspot.com/p/using-python-in-houdini.html)

[Houdini x Python](http://cdn2.gran-turismo.com/data/www/pdi_publications/CEDEC_2019_yamabe_PDI.pdf)