[MaxScript] 3dsmax でアニメーションしているコントローラを全部回収する

3dsmax でそのオブジェクトのどこかにキーが打たれているかどうか知りたい。簡単ですね。オブジェクトを選択してキーフィルタを「すべて」にし、タイムラインにキーが表示されればどこかにキーが打たれている。

さて、これをスクリプトからやり、ついでに、どこにキーが打たれているのかも取得したい、というのが今回のトピックです。例によって細かく作っていくので結果だけ欲しい人は最終的なコードまでジャンプしてください。

設計

最終的にオブジェクトを渡したらアニメーションしているコントローラを全部格納した配列が戻ってくる、という風にしたい。

ここで面倒くさそうなのは、3dsmax はほとんどすべてのコントローラがアニメーション可能(Animatable)で、さらにサブアニメーション(SubAnim)という仕組みで階層構造になっている、という点です。

さらに、モディファイヤを乗せてモディファイヤのアトリビュートにキーを打つこともできる。

そしてオブジェクトがどのような構造になっているかはオブジェクトによる、というわけで、再帰をうまいこと使ってなんとかしましょう。だいたいの方針は以下のようにします。

  • 最終的にオブジェクト(ノード)を渡してコントローラのリストを得る
  • コントローラを渡すとその下についているサブアニメーションを全部確認して、キーのあるものだけを回収する関数を作る
  • モディファイヤを渡すとそのサブアニメーションを全部確認して、キーのあるものだけを回収する関数を作る

最終的に使う関数の骨組

function getAllAnimatedCtrls obj = (
  /*
    オブジェクトのキーの打たれているコントローラをすべて回収する
    引数:
      object: 対象のオブジェクト
    戻り値:
      Array: キーのあるコントローラのリスト
  */
  local animated_ctrls = #()
    -- コントローラからサブアニメーションを回収するやつ
    -- モディファイヤからサブアニメーションを回収するやつ
  )
  return animated_ctrls
)

こんな感じ。最終目標から中の処理を考えて、1つずつ動作をイメージしていくのがわかりやすいと(僕は)思います。

オブジェクトが直接持っているコントローラからサブアニメーションを回収する

モディファイヤのことはひとまず置いておき、オブジェクトが直接持っているサブアニメーションについて考えます。代表的なものはトランスフォームコントローラですが、オブジェクトの種別によって持っているコントローラの種類は異なり、それによりサブアニメーションの種類や数も異なります。どんなものにでも対応できるような再帰関数を考えましょう。

function getAnimatedCtrls ctrl &animated_ctrls = (
  /*
    コントローラに含まれる、キーを持ったサブアニメーションコントローラを回収する
    引数:
      controller: 対象のコントローラ
      Array(参照): 戻り値を格納する配列
    戻り値:
      なし(第二引数に渡した配列に格納される)
  */
  -- 渡されたコントローラそれ自身にキーがある場合はそれを格納
  if ctrl.keys != undefined and ctrl.keys.count > 0 then (
    append animated_ctrls ctrl
  )
  -- サブアニメーションを回収
  -- いくつあるかわからないのでundefined になるまでwhile で回す
  local i = 1
  while ctrl[i] != undefined do (
    if ctrl[i].controller == undefined do (
      -- サブアニメーションにコントローラが無かったらスキップ
      i += 1
      continue
    )
    if ctrl[i].controller.keys.count > 0 do (
      -- キーがあれば格納
      append animated_ctrls ctrl[i].controller
    )
    -- 再帰
    getAnimatedCtrls ctrl[i].controller &animated_ctrls
    i += 1
  )
)

モディファイヤのサブアニメーションを回収する

モディファイヤの場合はモディファイヤのサブアニメーションコントローラを回収し、そのコントローラに対して上で作ったgetAnimatedCtrls を実行します。

function getSubAnimOfModifier mo &sub_anims = (
  /*
    モディファイヤのSubAnim を回収する
    引数:
      modifier: 対象のモディファイヤ
      Array(参照): 戻り値を格納する配列
    戻り値:
      なし(第二引数に渡した配列に格納される)
  */
  -- モディファイヤのサブアニメーションを回収
  -- いくつあるかはわからないので、undefined になるまでwhile で回す
  local i = 1
  while (mo[i] != undefined) do (
        if(mo[i][1] != undefined ) then (
            getSubAnimOfModifier mo[i] &sub_anims
        ) else (
            append sub_anims mo[i]
        )
        i+=1
    )
)

最初に骨組だけ作った関数を完成させる

function getAllAnimatedCtrls obj = (
  /*
    オブジェクトのキーの打たれているコントローラをすべて回収する
    引数:
      object: 対象のオブジェクト
    戻り値:
      Array: キーのあるコントローラのリスト
  */
  local animated_ctrls = #()
    -- コントローラからサブアニメーションを回収する
    getAnimatedCtrls obj.controller &animated_ctrls
    -- モディファイヤからサブアニメーションを回収する
    -- オブジェクトにモディファイヤがあったらモディファイヤ1つずつに対して実行する
    if obj.modifiers.count > 0 do (
        for m in obj.modifiers do (
            local mod_ctrls = #()
            getSubAnimOfModifier m &mod_ctrls
            for mc in mod_ctrls do (
                getAnimatedCtrls mc &animated_ctrls
            )
        )
    )
  return animated_ctrls
)

これを複数のオブジェクトに対して実行する

あとはこれを任意のオブジェクトに対して実行するだけ。選択したもの全部、シーン内のオブジェクト全部、等に対して実行する場合は以下のように取得した配列をjoin で連結すれば1つの配列にすべて格納できる。

-- 戻り値を集める配列
animated_ctrls = #()

-- 何らかのコレクションに対して実行する
-- 例:「選択オブジェクトに対して」
for o in selection do (
  join animated_ctrls (getAllAnimatedCtrls o)
)

もちろんオブジェクトごとに得た配列を配列にする、といったことも可能。

最終的なコード

function getSubAnimOfModifier mo &sub_anims = (
  /*
    モディファイヤのSubAnim を回収する
    引数:
      modifier: 対象のモディファイヤ
      Array(参照): 戻り値を格納する配列
    戻り値:
      なし(第二引数に渡した配列に格納される)
  */
  -- モディファイヤのサブアニメーションを回収
  -- いくつあるかはわからないので、undefined になるまでwhile で回す
  local i = 1
  while (mo[i] != undefined) do (
        if(mo[i][1] != undefined ) then (
            getSubAnimOfModifier mo[i] &sub_anims
        ) else (
            append sub_anims mo[i]
        )
        i+=1
    )
)


function getAnimatedCtrls ctrl &animated_ctrls = (
  /*
    コントローラに含まれる、キーを持ったサブアニメーションコントローラを回収する
    引数:
      controller: 対象のコントローラ
      Array(参照): 戻り値を格納する配列
    戻り値:
      なし(第二引数に渡した配列に格納される)
  */
  -- 渡されたコントローラそれ自身にキーがある場合はそれを格納
  if ctrl.keys != undefined and ctrl.keys.count > 0 then (
    append animated_ctrls ctrl
  )
  -- サブアニメーションを回収
  -- いくつあるかわからないのでundefined になるまでwhile で回す
  local i = 1
  while ctrl[i] != undefined do (
    if ctrl[i].controller == undefined do (
      -- サブアニメーションにコントローラが無かったらスキップ
      i += 1
      continue
    )
    if ctrl[i].controller.keys.count > 0 do (
      -- キーがあれば格納
      append animated_ctrls ctrl[i].controller
    )
    -- 再帰
    getAnimatedCtrls ctrl[i].controller &animated_ctrls
    i += 1
  )
)


function getAllAnimatedCtrls obj = (
  /*
    オブジェクトのキーの打たれているコントローラをすべて回収する
    引数:
      object: 対象のオブジェクト
    戻り値:
      Array: キーのあるコントローラのリスト
  */
  local animated_ctrls = #()
    -- コントローラからサブアニメーションを回収する
    getAnimatedCtrls obj.controller &animated_ctrls
    -- モディファイヤからサブアニメーションを回収する
    -- オブジェクトにモディファイヤがあったらモディファイヤ1つずつに対して実行する
    if obj.modifiers.count > 0 do (
        for m in obj.modifiers do (
            local mod_ctrls = #()
            getSubAnimOfModifier m &mod_ctrls
            for mc in mod_ctrls do (
                getAnimatedCtrls mc &animated_ctrls
            )
        )
    )
  return animated_ctrls
)


-- 呼び出し例
-- 戻り値を集める配列
animated_ctrls = #()

-- 何らかのコレクションに対して実行する
-- 例:「選択オブジェクトに対して」
for o in selection do (
  join animated_ctrls (getAllAnimatedCtrls o)
)
-- 取得したものの確認
print animated_ctrls

よかったらお使いくださいませ。