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

[前回の投稿](https://qiita.com/harayoki/items/98a7f8d8ced742df49a8)だと、メッシュとシェーディンググループ(SG)にコネクションがあっても、そのSGからシェーダーにコネクションがないパターンのマテリアル不正メッシュ状態を検知できていなかったので、その判定を盛り込んだ形で処理を修正しました。

合わせて、フェイスからSGにコネクションがない場合でもcompInstObjGroupsからSGにコネクションがあればシェーダーが外れた面が見えることはない＝compInstObjGroupsからSGにコネクションがあれば不正メッシュではない、と判定していた部分も上記の状態の場合正しいロジックとならなくなったので、compInstObjGroups判定を一番後ろに移動しました。

前回の記事は失敗例として、そのままにしておきます。

```python
import pymel.core as pm


sg_info = {}
"""SGに正しくシェーダーがくっついているかの情報をキャッシュするdict"""


def check_sg_and_shader_connected(sg):
    """
    シェーディンググループがシェーダーを接続されているかチェックする

    :type sg: pymel.nodetypes.ShadingGroup
    :rtype: bool
    """
    global sg_info
    if sg in sg_info:
        # 判定結果がキャッシュされていればそれを使う
        return sg_info[sg]
    cons = pm.listConnections(sg.surfaceShader, s=True, plugs=False)
    has_shader = (len(cons) > 0)
    sg_info[sg] = has_shader
    return has_shader

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':
            # shadingEngineが繋がっていてもそこからシェーダーが繋がっていないことがあるので確認
            return check_sg_and_shader_connected(con)
    return False


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

    :param shape: チェックするシェイプ
    :type shape: pymel.nodetypes.Shape
    :rtype: bool
    """
    return has_face_assign(shape) or has_object_assign(shape)


def has_any_assign2(shape):
    set_list = shape.listSets()
    for s in set_list:
        if s.type() == 'shadingEngine':
            return True
    return False


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

    :param shape: チェックするシェイプ
    :type shape: pymel.nodetypes.Shape
    :rtype: bool
    """
    # compInstObjGroupsからコネクションがSGに伸びていればフェイスアサインに抜けがあってもマテリアルがない面が現れることはない..わけではない
    # フェイス側でSGが付いているがそこにシェーダーが取れてる場合がある
    # よってまずフェイス面ごとにフラグを持ってマテリアルアサイン抜けがある部分を探す
    instanceNumber = shape.instanceNumber()
    indices = shape.instObjGroups[instanceNumber].objectGroups.getArrayIndices()
    numFaces = shape.numFaces()
    assigned_map = [False] * numFaces
    for i in indices:
        objectGroup = shape.instObjGroups[instanceNumber].objectGroups[i]
        cons = pm.listConnections(objectGroup, plugs=False)
        is_shadingEngine = len(cons) > 0 and cons[0].type() == 'shadingEngine'
        if not is_shadingEngine:
            # 違うものが繋がっていたらスルー(setなど)
            assigned_map[i] = True
            continue
        # shadingEngineが繋がっていてもそこからシェーダーが繋がっていないことがあるので確認
        has_shader = check_sg_and_shader_connected(cons[0])
        if not has_shader:
            # シェーダーが繋がっていないならば不正なシェイプ確定
            print('1')
            return False
        objectGrpCompList = objectGroup.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_:
                assigned_map[i] = True
    # 一つもFalseがなかったら全ての面にマテリアルが定義されている
    all_asigned = not (False in assigned_map)
    if all_asigned:
        return True
    # 面へのアサインに抜けがあってもデフォルトのシェーダー設定があればOKとする
    # compInstObjGroupsについてはよくわからない部分があるので、処理が怪しいかもしれない (index0だけ見ればいいのかも？)
    compInstObjGroups = shape.compInstObjGroups
    indices = compInstObjGroups.getArrayIndices()
    for i1 in indices:
        compObjectGroups = compInstObjGroups[i1].compObjectGroups
        indices2 = compObjectGroups.getArrayIndices()
        for i2 in indices2:
            compObjectGroup = compObjectGroups[i2]
            cons = pm.listConnections(compObjectGroup, d=True, plugs=False)
            is_shadingEngine = len(cons) > 0 and cons[0].type() == 'shadingEngine'
            if is_shadingEngine:
                has_shader = check_sg_and_shader_connected(cons[0])
                if has_shader:
                    return True
    # メッシュ全体へのデフォルトシェーダーもついてなければ不正とみなす
    return False


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 sg_info
del get_incomplete_mat_shapes
del is_face_assigned_all
del has_any_assign1
del has_any_assign2
del has_object_assign
del has_face_assign
del check_sg_and_shader_connected
```