mayaで使えるセーブポーズツール【Python】

今回はmayaで使える『ポーズのコピペツール』を作りました。 ポーズがコピペ出来るだけで、作業スピードは各段に早くなります。

ポーズコピペの機能紹介

ポーズのコピー&ペーストが主な機能になります。

ポーズのコピー

使い方は簡単で、

  1. リグを選び、

  2. ファイル名・保存先を入力し、

  3. ボタンを押すだけ。

たったのの3ステップでポーズを保存できます。

保存したポーズはUIに追加されるので、管理が楽です。

ポーズのペースト

適用させたいポーズをクリックするだけで、そのポーズをペーストできます。

別シーンでも利用可能

セーブされたポーズデータは、テキストファイルで外部に保存されてるため、シーンに依存することがありません。 そのため、Aシーンで使っていたポーズをBシーンで使う、ってことも出来ちゃいます。

セーブポーズのオススメな使い方

待機ポーズの保存&更新 指のバリエーションを保存

待機の保存

プロジェクトの途中で、待機ポーズが変わるっていうのは、よくありますよね... ポーズのコピペが出来たら、更新されたポーズデータを保存場所に入れるだけで、簡単にポーズの更新ができます。

指のバリエーションをまとめておく

グー・チョキ・パー、みたいな感じで指のセットを作っておく使い方もオススメです。 指の表情って、ポーズにおいてかなり重要なんですけど、0から形を作るってのは少し手間ですよね... よく使う指の形を保存しておけば、ざっくりとした形をペーストして、あとから微調整した方がかなり効率的です。

ポーズコピペの苦労話

  • 文字列結合によるキャストの重要性

  • Pythonのモジュールを活用したフォルダやファイルへのアクセス

  • 大量のポーズデータをUI上でどう管理するか

  • ファイルとアイコンのマッチング

今回は以上の部分に悩まされました。 順番に解説していきます。

文字列結合によるキャストの重要性

プログラムにおいて、数値と文字ってのは扱いが違います。 だから文字列を作る際に、数値と文字が混じっていたらエラーが起こるんですよね...

こんな場合に使う方法が『キャスト』っていう処理。 数"値"→数"字"に変換するイメージです。

#キャストのイメージ
num = 6
castNum = str(num)

print (type(num))
#結果<type 'int'>

print type(castNum)
#結果<type 'str'>

numって変数に入ってるのがint型、つまり数値です。 一方でcastNumに入ってるのはstr型、これは数字(文字列)です。

#実際の使用例
iconBtn = cmds.iconTextButton("iconBtn"+str(i),st='iconAndTextHorizontal',
i1=nonIcon,l=name000,c="inFile("+str(i)+")")

今回は数値を含んだ文字列の結合が、頻繁に必要になりました。 上のソースはUIのボタンを作る処理ですが、ボタン識別用と関数の処理を分ける際に、キャストを使った文字列の結合してます。

キャストなんて、分かっていないとエラーの沼に一直線です... 『型』への意識、その重要性を改めて感じる良い機会でした。

Pythonのモジュールを活用したフォルダやファイルへのアクセス

ポーズデータは外部にテキストファイルを作っているのですが、その際にPythonの色んなモジュールを活用しました。

■今回使用したモジュール

  • os

  • os.path

  • glob

  • ast

osモジュールの活用例

#osモジュールの活用例
os.listdir("ファイルパス")
# 結果: ['000_finger.txt', '001_finger.txt']

『os.listdir(ファイルパス)』では指定したフォルダ内のファイルを調べ、取得したポーズデータを調べてます。 ちなみに『000_finger.txt』っていうテキストファイルに、ポーズデータが入ってます。

os.pathモジュールの活用例

#os.pathモジュールの活用例
os.path.basename('000_finger.txt').split('.', 1)[0]
# 結果: 000_finger

『os.path.basename('000_finger.txt').split('.',1)[0]』は、『.txt』って拡張子を除く処理をしてます。 ボタンの名前をファイル名にする際『.txt』が邪魔だったので、それを省きました。

globモジュールの活用例

#globモジュールの活用例
glob.glob("ファイルパス")
# 結果: ['ファイルパス\\000_finger.txt', 'ファイルパス\\001_finger.txt']

『glob.glob("ファイルパス")』は、ファイルパスを含んだファイル名を取得してます。 これを使い、指定したファイルからポーズデータを持ってきて、ポーズに適用させました。

#astモジュールの活用例
ast.literal_eval("ポーズデータ")#ディクト型に変換
# 結果: {'L_thumb01_Ctrl.translateY': 0.00497, 'L_thumb01_Ctrl.translateX': 0.0042}

『ast.literal.eval('ポーズデータ')』は、読み込んだテキストファイルをディクト型に変換する処理をしてます。 テキストファイルには『コントローラー名+アトリビュート名+数字』って感じでデータが入ってます。

ポーズデータの例. {'L_thumb01_Ctrl.translateY': 0.00497, 'L_thumb01_Ctrl.translateX': 0.0042}

このただの文字列を、ディクト型に変換してくれるのがastモジュールというわけです。 こうやって変換してくれたおかげで、コントローラーと値の関係がひとまとめになり、どのコントローラーにどの値を入れたらいいのか、って処理が楽に実装出来ました。

aetモジュールさん、マジで神ですね~

os系のモジュールが大量に必要で、そこを調べるのが苦労しました。

大量のポーズデータをUI上でどう管理するか【プルダウンとスクロール】

保存するポーズの数に制限があるといけないので、大量管理を前提にUI設計しました。 スクロールを採用することで、実質、無制限にポーズを保存することができます。

ファイルとアイコンのマッチング

for i in range(len(fileName000)):
    name000 = os.path.basename(fileName000[i]).split('.', 1)[0]
    icon = iconPath000+name000+".jpg"
        
    if(os.path.exists(icon)):
        iconBtn = cmds.iconTextButton("iconBtn"+str(i),st='iconAndTextHorizontal',
        i1=icon,l=name000,c="inFile("+str(i)+")",fla=1)#ファイル名とマッチしたアイコンのみ表示
    else:
        nonIcon = iconPath000+"non.jpg"
        iconBtn = cmds.iconTextButton("iconBtn"+str(i),st='iconAndTextHorizontal',
        i1=nonIcon,l=name000,c="inFile("+str(i)+")")

上の方で紹介したos.pathモジュールを活用し、ポーズデータのファイル名とアイコン画像のマッチングを調べ、アイコン画像がある場合とない場合で処理を分けてます。 これを分岐させておかないと、アイコン画像がない場合、処理が止まっちゃうんですよね...

UIの処理はまだまだ追加要素があるので、引き続きブラッシュアップしていきます。

今後の改善

■追加・改善ポイント

  • ファイルの消去

  • フォルダ分け機能

  • スクショ撮影用のカメラを別で用意

  • オフセット機能

保存したポーズを消したい場合に、UI上で対応できてません。 右クリックで削除か、削除ボタンの実装か、、、 いずれにせよ、改善を加える必要があります。

また、今後はフォルダ分けを実装して、プルダウンごとにポーズ集を管理できるようにします。 今のままだと、整理ができないですからね...

さらに、スクショを普通に撮ろうとすると、被写体がかなり小さくなるんですよね... これを改善するには、スクショ用のカメラを用意して、そのカメラで撮影してあげる必要がありそうです。

最後は、トランス方向にオフセットが付けられるようにしたいですね。 移動終わりに待機ポーズを合わせたい場合、今のままじゃ原点に来ちゃうんで、使い勝手が悪いです。

今のところは、以上を追加・改善していこうと考えてます。

ソースコード公開

改編・再配布は自由ですが、自己責任でお願いします。 ※パスはご自身の環境に合わせてください

import os.path
import os
import maya.cmds as cmds
import glob
import ast
import sys

def KK_SevePoseUI():    
    filePaht000 = "C:/work/maya/savePose/poseDate/"
    iconPath000 = "C:/work/maya/savePose/img/"
    
    fileName000 = os.listdir(filePaht000)#ポーズファイルのリスト
    iconName000 = os.listdir(iconPath000)#アイコンファイルのリスト
    
    w = 400
    h = 200
    windowTitle = "KK_SavePoseWindow"
    
    if(cmds.window(windowTitle,ex=1)):
        cmds.deleteUI(windowTitle)

    win = cmds.window(windowTitle,t="KK_SavePose",wh =(w,h))
    sp_mainForm = cmds.formLayout("sp_mainForm")    
    sp_frame00 = cmds.frameLayout("sp_frame00",p=sp_mainForm,lv=0)
    
    topBtnForm = cmds.formLayout("topBtnForm",p=sp_frame00) 
    fileName01 = cmds.textFieldGrp("fileName01",l='ファイル名',p=topBtnForm)
    savePath = cmds.textFieldGrp("savePath",l="保存場所",text=filePaht000,p=topBtnForm)
    applyBtn = cmds.button("applyBtn",l="セーブポーズ",c="getValue()",p=topBtnForm)
    #cmds.button(l="更新")ボタンで更新できるようにしたい...
    
    cmds.setParent("..")
    
    sp_frame01 = cmds.frameLayout("sp_frame01",cll=1,p=sp_mainForm,l="POSE")
    btnForm = cmds.formLayout("btnForm",p=sp_frame01)
    btnScroll = cmds.scrollLayout("btnScroll",p=btnForm)
    #farme001 = cmds.frameLayout("farme001",cll=1,l="pose01")
    for i in range(len(fileName000)):
        name000 = os.path.basename(fileName000[i]).split('.', 1)[0]
        icon = iconPath000+name000+".jpg"#ここでポーズデータのファイル名から、アイコンのファイル名を生成する必要がある
        print icon
        
        if(os.path.exists(icon)):
            iconBtn = cmds.iconTextButton("iconBtn"+str(i),st='iconAndTextHorizontal',
            i1=icon,l=name000,c="inFile("+str(i)+")",fla=1)#ファイル名とマッチしたアイコンのみ表示
        else:
            nonIcon = iconPath000+"non.jpg"
            iconBtn = cmds.iconTextButton("iconBtn"+str(i),st='iconAndTextHorizontal',
            i1=nonIcon,l=name000,c="inFile("+str(i)+")")
            print "無し"
    
    #cmds.setParent("..")
    cmds.formLayout("topBtnForm",e=1,
                    af=[(savePath,"right",0),(savePath,"left",0)],ac=(savePath,"top",5,fileName01),an=(savePath,"bottom"))
                    
    cmds.formLayout("topBtnForm",e=1,
                    af=[(applyBtn,"right",0),(applyBtn,"left",0),(applyBtn,"bottom",5)],ac=(applyBtn,"top",5,savePath))
                                     
    cmds.formLayout("sp_mainForm",e=1,
                    af=[(sp_frame00,"top",5),(sp_frame00,"right",5),(sp_frame00,"left",5)],an=(sp_frame00,"bottom"))
         
    cmds.formLayout("sp_mainForm",e=1,
                    af=[(sp_frame01,"bottom",5),(sp_frame01,"right",5),(sp_frame01,"left",5)],ac=(sp_frame01,"top",5,sp_frame00))
    
    #ボタンスクロールのレイアウト編集
    cmds.formLayout("btnForm",e=1,
                    af=[(btnScroll,"top",5),(btnScroll,"bottom",5),(btnScroll,"right",5),(btnScroll,"left",5)])    
    
    cmds.showWindow(win)
#----実行ボタンの処理--------/
def getValue(): 
    fileName01 = cmds.textFieldGrp("fileName01",q=1,tx=1)#ファイル名と保存場所を取得
    savePath = cmds.textFieldGrp("savePath",q=1,tx=1)
    
    selNode = cmds.ls(sl=1)
    print selNode
    if len(selNode) == 0 and len(fileName01) == 0:
        cmds.warning("オブジェクトを選択してファイル名を付けてください")
        sys.exit()
    elif len(selNode) == 0:
        cmds.warning("オブジェクトを選択してください")
        sys.exit()        
    elif len(fileName01) == 0:
        cmds.warning("ファイル名がありません")
        sys.exit()
    elif len(savePath) == 0:
        cmds.warning("保存先を指定してください")
        sys.exit()       
                 
    #keyAttList = cmds.listAttr(selNode,s=1,w=1,k=1,v=1,u=1,st=['translate*','rotate*','scale*'])#アトリビュートの取得
    
    #選択したノードとアトリビュートをとってくる
    keyAttList = cmds.listAttr(selNode,s=1,w=1,k=1,v=1,u=1,st=['translate*','rotate*','scale*'])#アトリビュートの取得    
    nameDic = "{"
    for nodeList in selNode:
        keyAttList = cmds.listAttr (nodeList, r=True, w=True, k=True, u=True, v=True, m=True, s=True)#アトリビュートの選別 
        if selNode is None:
                continue#trueならば繰り返しを抜ける
                
        elif len(selNode) == 0:
                continue  
        print keyAttList   
        for attr in keyAttList:
            value = cmds.getAttr(nodeList+"."+attr)
            nameDic = nameDic+"\""+nodeList+"."+attr+"\":"+str(value)+",\n"
            
    nameDic = nameDic+"}\n"   
    #------取ったデータをテキストファイルへ書きだし----
    filePath = savePath+"\\"+fileName01+".txt"
    
    if os.path.isfile(filePath):
        cmds.warning("同じ名前のファイルがあります")
        sys.exit()
    
    s = nameDic
    with open(filePath,mode ='w') as f:#ファイル書きだしテスト
        f.write(s)
    #--------プレイブラストでスクショとる
    #現在のフレームを取得
    cmds.select(cl=1)
    time = int(cmds.currentTime(q=1))
       
    iconPath = "C:/work/maya/savePose/img/"
    paseName = cmds.textFieldGrp("fileName01",q=1,tx=1)#UIを参照してファイル名をとってくる 
    icon = iconPath+paseName+".jpg"#ボタンにスクショを適用させる
    
    cmds.modelEditor("modelPanel4",e=1,nc=0)#ナーブスカーブを非表示
    
    poseIcon = cmds.playblast(fmt="image",c="jpg",w=70,h=70,
                st=time,et=time,cf=iconPath+paseName,fo=1,v=0,p=100,qlt=100,cc=1,sqt=0,
                showOrnaments=0)#ファイル名にパスを含めると、そこに書き出してくれる。便利~
    os.rename(poseIcon,poseIcon+".jpg")
    cmds.modelEditor("modelPanel4",e=1,nc=1)#ナーブスカーブ表示
    
#--------------ポーズを読み込むときの処理----------------#
    #------アトリビュートの読み込み、セットしていく-------#
def inFile(i):
    filePath = "C:/work/maya/savePose/poseDate/*"
    
    File = glob.glob(filePath)
    
    f = open(File[i])
    inf = f.read()
    dicDate = ast.literal_eval(inf)#ディクト型に変換
    
    key = [k for k,v in dicDate.items()]
    val = [v for k,v in dicDate.items()]

    for i in range(len(key)):
        cmds.setAttr(key[i],val[i])
    
    f.close()
KK_SevePoseUI()
← ブログへ戻る