Maya Dependency GraphをPython APIで走査する

# ディペンデンシーグラフをAPIでたどる

Mayaの仕事をしていると、「Shading Networkを調べるため全部スキャンする」なんてことが仕事でよくあるので、Python APIでできないか調べてみました。
Dagノードの階層を走査するサンプルはよくあるのですが、ディペンデンシーノードグラフ（DG）のスキャンはあまりないのでメモしておきます。(ちなみにDagはDGの一部。)

また、コマンドで同じ処理を書いた場合とのスピードを比較してみました。

## DG用イテレータ

DGにも走査するためのイテレータが用意されています。
OpenMayaモジュールのMItDependencyGraphを使います。→ [MItDependencyGraph](http://help.autodesk.com/view/MAYAUL/2018/JPN/?guid=__py_ref_class_open_maya_1_1_m_it_dependency_graph_html)

```python
dependIter = om.MItDependencyGraph(sg, 
                                   om.MFn.kInvalid, 
                                   om.MItDependencyGraph.kUpstream)
```

API1.0と2.0で渡すオブジェクトが異なるので注意が必要です。
1.0だと`MFnDependencyNode`、2.0だと`MObject`です。

## サンプルコード

指定されたオブジェクトからそれに使用されているテクスチャをリストするスクリプトを作成してみました。

処理の大まかな流れは
1. 指定されたオブジェクトから、その下位階層のメッシュリストを取得
2. 各オブジェクトにアサインされているShadingGroupを取得
3. ShadingGroupをルートノードとして、上流のDGノードを辿っていく
4. テクスチャノードが見つかったらストアする

ここではAPI1.0を使用して、スキャンスクリプトを書いてみます。

```python:dg_traverse_api1.py
# -*- coding: utf-8 -*-
from maya import OpenMaya as om
import maya.cmds as cmds
def get_texture_from_nodes(nodes):
	## オブジェクト名のリストから、実際のメッシュリストを取得
	cmds.select(cmds.ls(nodes, dag=1, s=1))
	mesh_tex_dict = {}
	selection_list = om.MSelectionList()
	om.MGlobal.getActiveSelectionList(selection_list)
	## 選択オブジェクトを走査するイテレータを作成
	shape_iter = om.MItSelectionList(selection_list)
	
	## すべてのメッシュを検査するまでループ
	while not shape_iter.isDone():
		item_type = shape_iter.itemType()
		if item_type == om.MItSelectionList.kDagSelectionItem:
			dag_path = om.MDagPath()
			shape_iter.getDagPath(dag_path)
			if dag_path.hasFn(om.MFn.kMesh):
				mesh_fn = om.MFnMesh(dag_path.node())
				sgs = om.MObjectArray()
				mesh_compos = om.MObjectArray()
				## メッシュにアサインされているShadingGroupを取得
				mesh_fn.getConnectedSetsAndMembers(dag_path.instanceNumber(),
												sgs,
												mesh_compos,
												True)
				textures = []
				for i in range(sgs.length()):
					sg_fn = om.MFnDependencyNode(sgs[i])
					## ShadingGroupから上流のDGノードを検査するためのイテレータを作成
					dependIter = om.MItDependencyGraph(sgs[i],
													om.MFn.kInvalid,
													om.MItDependencyGraph.kUpstream)
					while not dependIter.isDone():
						node = dependIter.currentItem()
						depNode = om.MFnDependencyNode(node)
						## ノードタイプを調べて、fileかaiImageならtextureリストに追加
						if depNode.typeName() in ["file", "aiImage"]:
							textures.append(depNode.name())
						dependIter.next()
				## mesh名とtextureリストをセットにして辞書にストア
				mesh_tex_dict.update({dag_path.fullPathName():textures})
		shape_iter.next()
	return mesh_tex_dict
```

このコードを以下のように実行すると、メッシュとテクスチャの辞書が返ってきます。

```python
import dg_traverse_api1 as dgapi1
tex_info = dgapi1.get_texture_from_nodes(["ROOT"])
print tex_info
>> {u'|ROOT|pCube1|pCubeShape1': [u'file1', u'file2']}
```

`ROOT`はシーン内のオブジェクト名です。

# スピード検証
mayaコマンドでDGをスキャンしたスクリプトとの比較を行ってみました。
スクリプトの処理としては以下の4パターン

- listHistoryコマンドを用いた検出
- listConnectionsコマンドで再帰的にコネクションをたどる検出
- 上記のAPI1.0スクリプト
- 上記のAPI2.0バージョン

使用したシーンは、pCubeを100個用意し、以下のような適当なShadingNetworkを100個複製し、それぞれアサインしたものです。

<img width="1117" alt="sample_network.png" src="https://qiita-image-store.s3.amazonaws.com/0/240509/c670ded5-d462-bff7-6455-deae94ca4ac8.png">

各スクリプトを以下に載せます。

**listHistory**

```python:dg_traverse_cmdshistory.py
# -*- coding: utf-8 -*-
import maya.cmds as cmds

def get_texture_from_nodes(nodes):
	shapes = cmds.ls(nodes, dag=1, s=1, l=1)
	mesh_tex_dict = {}
	for shape in shapes:
		textures = []
		sgs = cmds.listConnections(shape, type="shadingEngine")
		if sgs:
			for sg in sgs:
				histories = cmds.listHistory(sg)
				for his_node in histories:
					if cmds.objectType(his_node) in ["file", "aiImage"]:
						textures.append(his_node)
		mesh_tex_dict.update({shape:textures})
	return mesh_tex_dict
```

**listConnections**

```python:dg_traverse_cmdsconnections.py
# -*- coding: utf-8 -*-
import maya.cmds as cmds

def get_texture_from_nodes(nodes):
	shapes = cmds.ls(nodes, dag=1, s=1, l=1)
	mesh_tex_dict = {}
	material_types = cmds.listNodeTypes('shader')
	texture_types = cmds.listNodeTypes('texture')
	utility_types = cmds.listNodeTypes('utility')
	shading_types = material_types + texture_types + utility_types + ["shadingEngine"]
	shading_types = list(set(shading_types))
	for shape in shapes:
		textures = []
		sgs = cmds.listConnections(shape, type="shadingEngine")
		if sgs:
			_find_textures(sgs, textures, shading_types)
		mesh_tex_dict.update({shape:textures})
	return mesh_tex_dict


def _find_textures(nodes, queue, filters):
	for node in nodes:
		node_type = cmds.objectType(node)
		if node_type not in filters:
			continue
		if node_type in ["file", "aiImage"]:
			queue.append(node)
		children = cmds.listConnections(node, s=1, d=0) or []
		if children:
			_find_textures(children, queue, filters)
```
**API2.0**

```python:dg_traverse_api2.py
# -*- coding: utf-8 -*-
from maya.api import OpenMaya as om
import maya.cmds as cmds
def get_texture_from_nodes(nodes):
	cmds.select(cmds.ls(nodes, dag=1, s=1))
	mesh_tex_dict = {}
	selection_list = om.MGlobal.getActiveSelectionList()
	shape_iter = om.MItSelectionList(selection_list)
	while not shape_iter.isDone():
		item_type = shape_iter.itemType()
		if item_type == om.MItSelectionList.kDagSelectionItem:
			dag_path = shape_iter.getDagPath()
			if dag_path.hasFn(om.MFn.kMesh):
				mesh_fn = om.MFnMesh(dag_path.node())
				shaders, polyindeces = mesh_fn.getConnectedShaders(dag_path.instanceNumber())
				textures = []
				for sg in shaders:
					dependIter = om.MItDependencyGraph(sg, om.MFn.kInvalid, om.MItDependencyGraph.kUpstream)
					while not dependIter.isDone():
						node = dependIter.currentNode()
						depNode = om.MFnDependencyNode(node)
						if depNode.typeName in ["file", "aiImage"]:
							textures.append(depNode.name())
						dependIter.next()
				mesh_tex_dict.update({dag_path.fullPathName():textures})
		shape_iter.next()
	return mesh_tex_dict
```

## 結果
| スクリプト        |  time(sec) |
|:-----------------|:-----------|
| listHistory      | 0.069798   |
| listConnections  | 0.812645   |
| API1.0           | 0.036853   |
| API2.0           | 0.032879   |

Maya2018で上記スクリプトを10回実行した平均値です。
API2.0がやはり一番速かったですが、listHistoryも結構速いですね。
ただ経験上listHistoryは最上流まできちんとtraverseしてくれないケースがあるので、あまり使わないです。
今回API2.0で書いてみて意外とPythonicに書けるので直感的に書きやすく、しかも速いというので、これからは2.0だなという感想です。