【MAYA】マテリアルのアサインが不完全なシェイプを探す

ここ何回かに作った関数を組み合わせて、シーン上にマテリアルのアサインが（フェイスの１枚でも）抜けているメッシュがないか確認するscriptを作りました。同じようなループ処理をいろんなところで繰り返しているので、重そうではありますが一応使えます。
フェイスが多くて重たそうな時は、一部選択して探索領域を狭められるのでそうするといいかもしれません。

#使い方

<img width="337" alt=" 2018-01-11 2.50.08.png" src="https://qiita-image-store.s3.amazonaws.com/0/8302/f5c57d16-2a13-b236-8e8f-b713cc611923.png">
このような（手抜きの）例で実行してみます。蛍光グリーンの面が見えているメッシュが不正メッシュです。片方がオブジェクトごとマテリアルのアサインがなく、片方は一部のフェイスだけマテリアルのアサインがありません。青いキューブの右下の２つは左の２つをコピーしてグループで包んで階層を深くしてみたものです。

このまま何も選択せず、後述のスクリプトを実行します。（シーン全てが探索対象。）

<img width="377" alt=" 2018-01-11 2.49.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/8302/d8ddb73c-48e4-305b-8be6-a1be16440743.png">

もしくは、探索したいツリーの上位ノードを選択してやってから、スクリプトを実行します。

<img width="356" alt=" 2018-01-11 2.50.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/8302/1d46634e-eb1d-09fd-d7ae-c9646f1f6000.png">
すると、不正メッシュだけが選択状態になります。

結果は scriptログに以下のような感じでも表示されます。

```text
探索対象shape一覧______
pCube1|pCubeShape1
pCube2|pCubeShape1
pCubeShape3
pCubeShape4
pCubeShape5
pCubeShape6
pasted__pCube1|pasted__pCubeShape1
pasted__pCube2|pasted__pCubeShape1

マテリアルアサイン不正のshape一覧__
pCubeShape5
pCubeShape6
```

#処理のポイント
普通にメッシュ一覧を`ls`で取る場合はインスタンスがあるメッシュは１つのメッシュとしてまとまって返ってきてしまうので、フェイス単位でのマテリアルチェックができません。（正確に言うとできるが、ヒエラルキー上でどこにあるのか探しづらくなる。）その辺をよしなに作ってあります。`get_incomplete_mat_shapes`関数以外の注意点は他の関連投稿をみてください。

#クイック実行用ソース
scriptパネルやシェルフに登録する想定です。
しかし長いな。。しかし、頑なにモジュール化したりギハブにソースをあげる事を避ける検証シリーズなので、このまま行きます。

:exclamation:(判定に一部問題があったので[次の記事](https://qiita.com/harayoki/items/f02398b7e0d952a4388c)で修正をいれています。)

```python
import pymel.core as pm


def has_face_assign(shape):
    """
    シェイプにマテリアルのフェイスアサインがあるかどうか確認する

    :param shape: チェックするシェイプ
    :type shape: pymel.nodetypes.Shape
    :rtype: bool
    """
    instanceNumber = shape.instanceNumber()
    objectGrpCompList = \
        shape.instObjGroups[instanceNumber].objectGroups[0].objectGrpCompList
    num_face_block = (len(objectGrpCompList.get()))
    return num_face_block > 0


def has_object_assign(shape):
    """
    シェイプにマテリアルのオブジェクトアサインがあるかどうか確認する

    :param shape: チェックするシェイプ
    :type shape: pymel.nodetypes.Shape
    :rtype: bool
    """
    instanceNumber = shape.instanceNumber()
    instObjGroup = shape.instObjGroups[instanceNumber]
    cons = pm.listConnections(instObjGroup, plugs=False)
    for con in cons:
        if con.type() == 'shadingEngine':
            return True
    return False

def is_face_assigned_all(shape):
    """
    フェイスアサインの抜けがあって、マテリアル未定義面ができていないか確認する

    :param shape: チェックするシェイプ
    :type shape: pymel.nodetypes.Shape
    :rtype: bool
    """
    # compInstObjGroupsからコネクションがSGに伸びていればフェイスアサインに抜けがあってもマテリアルがない面が現れることはない
    indices = shape.compInstObjGroups.getArrayIndices()
    found = False
    for i in indices:
        group = shape.compInstObjGroups[i].compObjectGroups[0]
        cons = pm.listConnections(group, plugs=True)
        if len(cons) > 0 and cons[0].type() == 'shadingEngine':
            found = True
            break
    if found:
        return True
    # フェイス面ごとにフラグを持ってマテリアルアサイン抜けがある部分を探す
    instanceNumber = shape.instanceNumber()
    indices = shape.instObjGroups[instanceNumber].objectGroups.getArrayIndices()
    numFaces = shape.numFaces()
    assined_map = [False] * numFaces
    for i in indices:
        objectGrpCompList = shape.instObjGroups[instanceNumber].objectGroups[i].objectGrpCompList
        faces_list = objectGrpCompList.get()  # ex) ['f[0]','f[1:3]']
        # 返ってきたフェイス情報が文字列なので力技で面番号のリストに変換する。。。
        for faces in faces_list:
            faces = faces[2:-1]  # ex) '0' or '1:3'
            if ':' in faces:
                a_ = [int(x) for x in faces.split(':')]  # ex [1,3]
                a_ = xrange(a_[0], a_[1] + 1)  # ex [1,2,3]
            else:
                a_ = [int(faces)]  # ex [0]
            # マテリアル設定があった面情報としてTrueをセット
            for i in a_:
                assined_map[i] = True
    #一つもFalseがなかったら全ての面にマテリアルが定義されている
    return not (False in assined_map)


def get_incomplete_mat_shapes(select_result=True):
    """
    マテリアルのアサインに１フェイスでも抜けがあるシェイプの一覧を返す

    :param select_result: 見つかった不正メッシュを選択状態にするか
    :type select_result: bool
    :return: 不正メッシュ一覧
    :rtype: list of pymel.nodetypes.Shape
    """
    check_target_shapes = []
    selected = pm.selected()

    def show_info(title, shapes):
        print('')
        print (title)
        if len(shapes) == 0:
            print(u'なし')
        for shape in shapes:
            print(shape)

    # 選択状態のノードがある場合はその中身だけを探索する
    if len(selected) > 0:
        # ノードのツリーは全部見るので子孫一覧を取る
        rels = pm.listRelatives(selected, allDescendents=True)
        # 最初の選択ノードも探索対象にする
        rels.extend(selected)
        for rel in rels:
            # meshなら探索対象
            if isinstance(rel, pm.nodetypes.Mesh):
                check_target_shapes.append(rel)
            elif isinstance(rel, pm.nodetypes.Transform):
                # 子供がmeshなら探索対象
                rel_shape = rel.getShape()
                if isinstance(rel_shape, pm.nodetypes.Mesh):
                    check_target_shapes.append(rel_shape)
        # 重複を取り除く
        check_target_shapes = list(set(check_target_shapes))
    else:
        # 選択状態のないノードがない場合は、シーンのmesh一覧を探索するが、
        # shapes = pm.ls(dep='mesh') # これだとインスタンスのmeshが一つにまとまってしまい全てが一覧に返ってこないので、
        # transform一覧を得てから
        transforms = pm.ls(type='transform')
        # 子供のmeshだけを集める
        check_target_shapes = [x.getShape() for x in transforms if isinstance(x.getShape(), pm.nodetypes.Mesh)]

    # ここからマテリアルのアサインチェック
    incomplete_shapes = []
    show_info(u'探索対象shape一覧______', check_target_shapes)
    for shape in check_target_shapes:
        if has_object_assign(shape):
            # オブジェクトアサインがある場合は不正でない
            continue
        elif has_face_assign(shape):
            # フェイスアサインがある場合で
            if not is_face_assigned_all(shape):
                # 抜けがある場合は不正である
                incomplete_shapes.append(shape)
        else:
            # なんのアサインもなければ不正である
            incomplete_shapes.append(shape)
    # 結果表示
    show_info(u'マテリアルアサイン不正のshape一覧__', incomplete_shapes)
    if select_result:
        pm.select(incomplete_shapes)
    return incomplete_shapes

get_incomplete_mat_shapes(select_result=True)
del get_incomplete_mat_shapes
del is_face_assigned_all
del has_object_assign
del has_face_assign
```