読者です 読者をやめる 読者になる 読者になる

Ueta CG Tech (blog)

Maya・MotionBuilderツール開発/ゲーム開発支援/映像制作支援/各種テクニカルスタッフ代行

【Maya】Python,C++,C#で比較。スキンウエイトの取得と設定

便利と評判の Python API 2.0 は、やっぱりスキンウエイトの処理も速かった。

Maya2016Python API 2.0 に、待望の MFnSkinCluster クラスが追加されました。
MFnSkinCluster を使用すると、Python API 2.0 でスキンウエイトを扱うことができます。

この記事では、各言語(またはモジュール)によって処理速度にどれくらいの差があるのか検証してみました。

比較データ

モデル A モデル B モデル C
頂点数 450,050 55,520 2,819
ジョイント 400 202 27

頂点数、ジョイント(インフルエンスオブジェクト)数の違う3種類のキャラクターモデルを用意しました。だいたいのイメージですが、モデルAはPS4/XOne世代シネマティック用キャラクター、モデルBは PS3/XBox360世代主人公クラスのキャラクター、モデルCはスマホゲーム向けキャラクターぐらいを想定しています。

比較した言語(モジュール)

今回、 maya.cmds, pymel, mel は対象外としております。

検証内容

  • スキンウエイトの取得と設定(対象:すべての頂点)

処理方法による違い

  1. MFnSkinCluster クラスが持つ getWeights(), setWeights() メソッドによる方法
  2. MFnSkinCluster クラスのアトリビュート(weightlist, weights)にアクセスする方法 




比較結果

横軸は経過時間()です。

f:id:ue_ta:20150823050216p:plain

◆処理方法1と処理方法2の違い

処理方法1のMFnSkinCluster.getWeights(),setWeights()を使用した方法(グラフ上)では、各言語の差がそれほどありませんが、 処理方法2のアトリビュートから取得・設定する方法(グラフ下)では、大きな差が出ているのがわかります。 これら二つの処理方法にはいくつかの相違点がありますが、もっとも重要な違いはメソッドを呼び出した回数です。

処理方法1では、 MFnSkinCluster.getWeights(),MFnSkinCluster.setWeights() を、それぞれ一度だけ呼び出しています。 このメソッドは、引数に頂点とジョイントのIDを渡すことで、自身のウエイト値を取得・設定することができます。

処理方法2では、 MFnSkinCluster.getWeights(), MFnSkinCluster.setWeights() に該当する動作を別の方法で行っています。 MFnSkinClusterの持つアトリビュートから、直接ウエイト値の取得と設定を行う方法です。これは処理方法1とは違い、反復処理によって一つずつ値を取得・設定していきます。 値を取得・設定するメソッド MPlug.asDouble()MPlug.setDouble() の呼び出し回数は

 頂点数 x ジョイントの数

となります。モデルAを例にすると
各頂点に対してすべてのジョイントのウエイト値を取得した場合

 450,050 * 400 = 180,020,000 

一般的に一つの頂点に影響を与えるジョイント数は限られており、モデルAでは最大値を 5 としたため

 450,050 * 5 = 2,250,250

MPlug.asDouble(), MPlug.setDouble()は、それぞれ 225万回 ほど呼び出されていることになります。 反復処理の中で呼び出されているメソッドは他にもあり、単に何らかのメソッドの呼び出し回数という意味では、この数字をはるかに上回ります。


同じPythonにもかかわらず圧倒的な差が開いている

ここで注目すべきは API1.0API2.0 の処理時間の差が大きいことだと思います。 同じPythonでありながらこれだけの差が出た原因は、 API1.0API2.0メソッドの呼び出し負荷(オーバーヘッド)に差があるからだと考えられます。



◆処理方法1と処理方法2ではどちらが速いのか

API1.0 の場合、どのモデルにおいても処理方法1の方が速いと考えてよさそうですが、実は問題があります(後述)
ここでは他の言語の場合を考えてみます。
モデルAでは、処理方法2 の方が 速い です。
モデルBでは、言語によって異なります。
モデルCでは、処理方法1 の方が 速い です。

どうやら、モデルBのあたりで逆転現象が起きているようです。

この原因の一つは、MFnSkinCluster.getWeights(), MFnSkinCluster.setWeights()が 引数に与えられた頂点、ジョイントに対するすべての値を取得・設定することにあると思います。

// 擬似コード

MIntArray weights;
SkinCluster.getWeights( vtxIDs , jointIDs , weights );
cout << weights.size() << endl;

// 180020000

モデルAを例にすると、上記のような方法で簡単に1億8千万要素の配列を得られますが、前述のとおり一つの頂点に影響を与えるジョイントの数は 5つ なので 実際には225万要素分にしか有効な値は入っていません。これは全体の 1.25% にすぎず、それ以外の 98.75% はすべてゼロで埋められているということになります。

各モデルにおける有効な要素数の割合は以下のとおり

  • モデルA: 1.25%
  • モデルB: 2.48%
  • モデルC:18.52%

この結果から、頂点数とジョイント数が多いほど無駄な値を返すことになるため、あえてそれを望んだのでなければ、必要なウエイト値をすべて取得する方法として良い方法ではないといえます。仮に、すべてのウエイト値を出力する必要があるなら、 全ての要素をゼロで初期化した配列に、有効な要素数だけ書き換えた方が速いでしょう。

ざっくりまとめると

  • 全体の要素数が少ないなら 処理方法1

  • 全体の要素数が非常に多いなら 処理方法2

すべてではなく、指定のウエイト値の取得の場合ではまた異なった結果になるかもしれません。それはまた別の機会に。

Python のループは注意が必要

PythonAPI1.0のグラフを見てみると、総じて処理方法1の方が速い結果となっています。 しかし、これには大きな罠があることがわかりました。以下に反復回数の多いループの処理時間の例を示します。

import timeit
setup='''
def foo(x):
  count = 0
  for i in xrange(x):
      count += 1
'''
t = timeit.Timer('foo(180020000)',setup)
print t.timeit(1),"s"
t = timeit.Timer('foo(2250250)',setup)
print t.timeit(1),"s"

# 12.6635769266 s
# 0.153748884231 s

Pythonで1億8千万回ループさせた結果、数字を加算するだけの処理で12秒かかっています。 スクリプトを書く場合、このループの中でif文を使ったり、何らかの処理をさせることがあると思いますが、 この遅さを考えると、要素数が多いケースでは結果的に処理方法2の方が速い可能性があります。


◆思いのほか API2.0 が優秀。なぜか getWeights(),setWeights()では C++ を押さえ API2.0 が速いという結果に

今回検証してみて API2.0 のポテンシャルの高さを垣間見た気がします。速くて便利なことは良いことです。ツールを作る側、使う側どちらにとってもありがたい話です。 それにしても、どうしても納得できない疑問が一つ、処理方法1で C++C# よりも API2.0 の方がやや速かったことです。初めは誤差かなと思いましたがすべてのモデルで同様の結果だったため、実際にそうなのだろうと思います。しかしC++より速いなんてことがあるのだろうかと思うのです。この辺の実装について詳しくないので、どなたかご存知の方いらっしゃいましたら教えていただきたいです。

API2.0 は API1.0 よりもオーバーヘッドで勝っている。というのが、今回の検証結果から見えた印象ですが、実際のところもっと検証が必要だと思います。スキンウエイト関連がたまたまそうだったという可能性が拭えません。

以上です。


ソースコード

※残りのコードは以下のリンクから

メモ書き:処理方法2のウエイト値のデータの型

  • C++:std::vectorを使った二次配列。配列は事前に生成。(push_backを用いて動的に確保した場合かなり遅くなった。また、連想配列のunordered_mapも試したが遅かった。通常の配列を二次配列newとして使用した場合はvectorよりもやや速かったが、自分でもメモリをdeleteする必要があったり若干不便だったのでvector採用)
  • C# : 辞書型(Dictionary)を使用。動的確保。(配列は試していない)
  • Python : 辞書型。dict[x]=y;として動的に生成。(静的確保した場合やリストを使った動的・静的二次配列などを試したがいまいちパフォーマンスが悪く、辞書型がわりと優秀という結論にいたったので辞書型を採用。しかし、単純なfor文で検証したときはリストの方が一次配列、二次配列ともに速かったのだが・・なぜだろう)

参考サイト様等

[C++] STLの型の使い分け - Qiita

C++で大規模な配列追記のパフォーマンス - ponkotuyの日記

Pythonで大規模な配列のパフォーマンス(append編) - ponkotuyの日記

C++ ハッシュ連想配列クラス unordered_map 入門

C++11 範囲ベース for ループ 入門

配列の動的確保

Maya Skincluster Weights

Maya Script OpenMaya ウェイトのインポート・エクスポート | 技研

デジタル・フロンティア-Digital Frontier | DF TALK | MayaPythonScriptを比較してみる

timeTest - sunzhongyuan9的专栏 - 博客频道 - CSDN.NET

PymelでWeight情報を取得する | Reincarnation+

glTools/skinCluster.py at master · bungnoid/glTools · GitHub

HTML5のcanvas機能で綺麗なグラフが作れる!「Flotr2」を試してみた | 株式会社LIG

flotr2 - documentation