Tips

modoでプロキシメッシュを作成するスクリプト

modoでウェイトマップからプロキシメッシュを作成するスクリプトをAIに作ってもらったので公開しておきます。

 

このスクリプトは、選択アイテムをウェイトマップごとにポリゴンを別アイテムに分解します。
アニメーション用のプロキシメッシュを作成する目的のスクリプトです。分解したアイテムはスケルトンにコンストレイントして使用します。modoはデフォーマの計算が重いですが、プロキシメッシュを使用することでアニメーションプレビューを快適に行えるようになります。

  • ウェイトは面に対して割り当てる前提なので、エッジにだけウェイトを設定しているモデルでは、ポリゴンが欠ける場合があります。
  • バイド済みのモデルの場合正しく動作しない場合があります。メッシュを別シーンにコピペした状態だと問題なく動くと思います。

@_kai_CreateProxyMesh.py

# !/usr/bin/env python
import lx
import lxu
import modo

def decompose_model_by_weight_maps(target_mesh=None):
    """
    ウェイトマップからインフルエンスを作成してモデルを分解する
    """
    try:
        # 現在のシーンを取得
        scene = modo.Scene()
        
        # ターゲットメッシュが指定されていない場合は選択から取得
        if target_mesh is None:
            selected_items = scene.selected
            
            if len(selected_items) == 0:
                lx.out("アイテムが選択されていません。")
                return
            
            # 最初に選択されたメッシュアイテムを処理
            target_item = None
            for item in selected_items:
                if item.type == 'mesh':
                    target_item = item
                    break
            
            if target_item is None:
                lx.out("メッシュアイテムが選択されていません。")
                return
        else:
            target_item = target_mesh
        
        lx.out("=== Weight Map Decomposer 実行開始 ===")
        lx.out("対象アイテム: {}".format(target_item.name))
        
        # 元のアイテムからメッシュデータをコピーして新しいアイテムを作成
        lx.eval('select.drop item')
        lx.eval('select.item {} set'.format(target_item.id))
        
        # 全てのポリゴンを選択
        lx.eval('select.polygon add 0 face')
        
        # メッシュデータをコピー
        lx.eval('select.copy')
        lx.out("メッシュデータコピー完了")
        
        # 新しいレイヤー(アイテム)を作成
        lx.eval('layer.new')
        
        # メッシュデータをペースト
        lx.eval('select.paste')
        
        # 新しく作成されたアイテムを取得
        current_items = list(scene.items())
        duplicate_item = None
        for item in current_items:
            if item.type == 'mesh' and item != target_item:
                # 最後に作成されたメッシュアイテムを取得
                if duplicate_item is None or item.id > duplicate_item.id:
                    duplicate_item = item
        
        if duplicate_item is None:
            lx.out("新しいアイテムが見つかりません。")
            return
        
        lx.out("新しく作成されたアイテム: {}".format(duplicate_item.name))
        
        # 新しく作成されたメッシュのウェイトマップを取得
        mesh_geometry = duplicate_item.geometry
        weight_maps = mesh_geometry.vmaps.weightMaps
        
        if len(weight_maps) == 0:
            lx.out("ウェイトマップが存在しません。")
            return
        
        lx.out(" ウェイトマップ数: {}".format(len(weight_maps)))
        
        # 元のアイテム名を保存
        original_name = target_item.name
        created_items = []
        
        # 処理対象を新しく作成されたアイテムに変更
        target_item = duplicate_item
        
        # 各ウェイトマップに対して処理を実行
        for i, weight_map in enumerate(weight_maps):
            weight_map_name = weight_map.name
            lx.out("\n処理中 ({}/{}): {}".format(i + 1, len(weight_maps), weight_map_name))
            
            try:
                # 処理前の全アイテムを記録
                initial_items = list(scene.items())
                initial_item_ids = [item.id for item in initial_items]
                
                # 1. 元のアイテムを選択
                lx.eval('select.drop item')
                lx.eval('select.item {} set'.format(target_item.id))
                
                # 2. ウェイトマップを選択
                lx.eval('select.vertexMap "{}" wght replace'.format(weight_map_name))
                
                # 3. ウェイトマップの値が0.01未満の頂点を除外
                lx.eval('vertMap.cull 0.01 false')
                
                # 4. インフルエンスを追加
                lx.eval('weightMap.addDeformer')
                
                # 5. 新しく作成されたgenInfluenceを特定
                current_items = list(scene.items())
                new_items = [item for item in current_items if item.id not in initial_item_ids]
                
                # genInfluenceタイプのアイテムを探す
                influence_item = None
                for item in new_items:
                    if item.type == 'genInfluence':
                        influence_item = item
                        break
                
                if influence_item is None:
                    lx.out("  警告: genInfluenceが見つかりません")
                    continue
                
                # 6. インフルエンスを選択してマップ選択
                success = select_influence_and_map(influence_item)
                if not success:
                    lx.out("  警告: インフルエンス選択に失敗")
                    cleanup_new_items_safe(scene, initial_item_ids)
                    continue
                
                # 選択された頂点数を確認
                selected_verts = lx.evalN('query layerservice verts ? selected')
                lx.out("  選択された頂点数: {}".format(len(selected_verts)))
                
                if len(selected_verts) == 0:
                    lx.out("  スキップ:  ウェイトマップ '{}' に対応する頂点が存在しません".format(weight_map_name))
                    cleanup_new_items_safe(scene, initial_item_ids)
                    continue
                
                # 7. 選択を頂点からポリゴンに変換
                lx.eval('select.convert polygon')
                
                # 選択されたポリゴン数を確認
                selected_polys = lx.evalN('query layerservice polys ? selected')
                lx.out("  選択されたポリゴン数: {}".format(len(selected_polys)))
                
                if len(selected_polys) == 0:
                    lx.out("  スキップ: ウェイトマップ '{}' に対応するポリゴンが存在しません".format(weight_map_name))
                    cleanup_new_items_safe(scene, initial_item_ids)
                    continue
                
                # 8. 選択されたポリゴンをカット
                lx.eval('select.cut')
                
                # 9. 新しいレイヤー(アイテム)を作成
                lx.eval('layer.new')
                
                # 10. カットしたポリゴンをペースト
                lx.eval('select.paste')
                
                # 11. 新しく作成されたメッシュアイテムを特定
                current_items_after_paste = list(scene.items())
                new_mesh_items = []
                for item in current_items_after_paste:
                    if item.id not in initial_item_ids and item.type == 'mesh':
                        new_mesh_items.append(item)
                
                if len(new_mesh_items) == 0:
                    lx.out("  警告: 新しいメッシュアイテムが見つかりません")
                    cleanup_new_items_safe(scene, initial_item_ids)
                    continue
                
                # 12. 新しいメッシュアイテムを選択
                new_mesh_item = new_mesh_items[0]
                lx.eval('select.drop item')
                lx.eval('select.item {} set'.format(new_mesh_item.id))
                
                # 13. 新しいアイテムの名前を設定
                new_item_name = "{}_{}".format(original_name, weight_map_name)
                lx.eval('item.name "{}"'.format(new_item_name))
                
                # 14. 作成された新規アイテム(genInfluence、TranEffector等)を削除
                cleanup_new_items_safe(scene, initial_item_ids, preserve_mesh=True)
                
                lx.out("  作成完了: {}".format(new_item_name))
                created_items.append(new_mesh_item)  # アイテムオブジェクトを保存
                
            except Exception as e:
                lx.out("  エラー -  ウェイトマップ '{}' の処理中: {}".format(weight_map_name, str(e)))
                try:
                    cleanup_new_items_safe(scene, initial_item_ids)
                except:
                    pass
                continue
        
        # 新しく作成されたソースアイテムを削除
        try:
            lx.eval('select.drop item')
            lx.eval('select.item {} set'.format(target_item.id))
            lx.eval('item.delete')
            lx.out("ソースアイテムを削除: {}".format(target_item.name))
        except Exception as e:
            lx.out("ソースアイテム削除エラー: {}".format(str(e)))
        
        # 全ての処理完了後に、作成された全てのメッシュのセンターを調整
        lx.out("\n=== センター調整開始 ===")
        for mesh_item in created_items:
            try:
                lx.eval('select.drop item')
                lx.eval('select.item {} set'.format(mesh_item.id))
                lx.eval('center.bbox center')
                lx.out("  センター調整完了: {}".format(mesh_item.name))
            except Exception as e:
                lx.out("  センター調整エラー: {} - {}".format(mesh_item.name, str(e)))
        
        lx.out("\n=== 処理完了 ===")
        lx.out("作成されたアイテム数: {}".format(len(created_items)))
        lx.out("処理された ウェイトマップ数: {}/{}".format(len(created_items), len(weight_maps)))
        for item in created_items:
            lx.out("  - {}".format(item.name))
            
    except Exception as e:
        lx.out("処理エラー: {}".format(str(e)))

def select_influence_and_map(influence_item):
    """
    インフルエンスアイテムを選択してマップを選択する
    """
    try:
        # アイテムとしてインフルエンスを選択
        lx.eval('select.drop item')
        lx.eval('select.item {} set'.format(influence_item.id))
        
        # デフォーマーマップを選択(頂点選択モードに切り替わる)
        lx.eval('select.deformerMap')
        
        return True
        
    except Exception as e:
        lx.out("  インフルエンス選択エラー: {}".format(str(e)))
        return False

def cleanup_new_items_safe(scene, initial_item_ids, preserve_mesh=False):
    """
    新規作成されたアイテムを安全に削除する
    preserve_mesh=Trueの場合、新しく作成されたメッシュは保持する
    """
    try:
        current_items = list(scene.items())
        items_to_delete = []
        
        for item in current_items:
            if item.id not in initial_item_ids:
                # 新しく作成されたアイテム
                if preserve_mesh and item.type == 'mesh':
                    # メッシュは保持
                    continue
                else:
                    items_to_delete.append(item)
        
        # アイテムを削除
        for item in items_to_delete:
            try:
                # アイテムが存在するかチェック
                if item in scene.items():
                    lx.eval('select.drop item')
                    lx.eval('select.item {} set'.format(item.id))
                    lx.eval('item.delete')
            except:
                # 削除に失敗しても処理を続行
                pass
                
    except Exception as e:
        lx.out("  クリーンアップエラー: {}".format(str(e)))

# メイン実行部分
try:
    lx.out("MODO Weight Map Decomposer")
    decompose_model_by_weight_maps()
    
except Exception as e:
    lx.out("メインエラー: {}".format(str(e)))

このスクリプトは10年以上前から欲しかった物ですが、AI様が作ってくれたので助かりました。
ACS3にも同様の機能が追加されたので便利に使えることを期待しましたが、ACS3でバインドしたモデルにしか使用できない物のようだったので、自分の用途には合いませんでした。他のソフトから持ってきたモデルを分解するような使い方もしたかった。

 

 

AIについて

前回よりも処理ステップ数の多いスクリプトにチャレンジしてみました。AIは引き続きclaudeを使用しています。

このスクリプトは最初、「ウェイトマップごとにポリゴンを分割するスクリプトを作って」のようなプロンプトを使用しました。頂点のウェイト値にアクセスしてウェイト値が一定以上のポリゴンを選択しようとして、当然のように頂点アクセス部分でエラーが発生しました。
スクリプトとしてはよくある処理だと思うのですがmodoでは上手く行きそうな気がしなかったので、modo標準のコマンドを使用するスクリプトになるように指示しました。

具体的には処理内容をmodoのマクロで作れそうな手順にして、処理内容を1行ずつコマンドを含めて書いてスクリプト作成を指示しました。
スクリプトのコメントに番号がある物が最初にマクロ的に指示した名残です(全て指示したものではなく、AIが追加してる部分もあります)。

このスクリプト作成でAIが躓いたのはジェネラルインフルエンスの選択でした。ウェイトが割り当てられたポリゴンを選択するために、一度ウェイトマップからデフォーマを追加しているのですが、ポリゴンのカット&ペースト後に不要なデフォーマを削除する必要がありました。
AIはインフルエンスのアイテムタイプがわからなくてエラーを出していたので、インフルエンスを選択したときに「コマンド履歴」に表示される「genInfluence」というのがアイテムタイプっぽかったので、「アイテムタイプはgenInfluenceでどう?」のようにAIをアシストしてエラーを解消しました。

コマンドと引数のようなmodo固有の部分はエラーが発生しやすいですが、アイテムのリネームやウェイトマップの数だけ繰り返す処理はすんなり作ってくれたように思います。マクロのように処理順にコマンドを記述して指示を出すと、様々なmodo用のスクリプトを作れそうです。

コメントを残す