MayaからUnityへのUVアニメーションエクスポート

はじめに

こんにちは、セガ・インタラクティブ 第三研究開発本部 開発一部
ソフトウェアエンジニアのサカイと申します。
最近はアーケードゲームの開発をしています。

本日は、「MayaからUnityへのUVアニメーションエクスポート」
というテーマで書いていきます。作業記録のような書き方ですが、私の好みなので許してください。
いつもの作業の流れはこんな感じなのです。

まず、動機、計画について簡単にまとめた後、
本題のMayaからのエクスポートとUnityのインポートについて書きます。
最後に、まとめと今後の拡張案で締めます。

なるべく内容に間違いのないように努めますが、
必ず自身で調べて、確認するようお願いします。

概要

動機

現在、私のチームのアーティストはMayaでモデリングとアニメーションつけを行っています。
アニメーション作業の中にはUVアニメーションによるキャラクターの表情変化が含まれます。
しかし、Unityの標準機能ではUVアニメーションをMayaからインポートする方法がなさそうです。

Unity上で表情アニメーションだけ後付けすることは手間がかかりますし、不具合の元にもなります。
アーティストの要望は以下のような感じでしょうか。

  • Maya上でUVアニメーションもつけたい
  • Unityにインポートした後で手を加える必要がない

計画

少し調べたところでは、既存の拡張はなさそうです。位置等の別チャネルに入れる方法は手違いが生じそうです。
自作するしかないでしょう。下記2つの機能を使えば要望を満たせそうです。これらを使ってできない場合は、
別ファイルにUVアニメーションを出力 ⇒ インポート時に統合で実現は可能でしょう。プランBは常に考えておきます。

作業見積もりは、1日4~5時間作業できたとして、4日ぐらいでしょうか。調査が必要ですので長めに見積もっています。
後で不具合や修正要望が出たら片手間でも対応できるでしょう。

Mayaのエクスポート

”FBX Extensions Software Development Kit”というものが Autodeskさんから提供されています。
これを使うと、FBXのエクスポートのタイミングでFBXファイルの加工ができます。
Maya上のユーザデータを汚すこともなさそうです。

Unityのインポート

Unityには、FBXのロード後にユーザデータと共にフックする関数が用意されています。
AssetPostprocessor.OnPostprocessGameObjectWithUserProperties(Unityマニュアル)

適当なボックスを作成してテキスト形式のFBXで出力してみます。
以下のようにFBXファイルを修正すると、Unityでキーが”my_property”、値が”test”で取得できます。

Model: 1074780944, "Model::pCube1", "Mesh" {
    Version: 232
    Properties70:  {
        P: "RotationActive", "bool", "", "",1
        P: "InheritType", "enum", "", "",1
        P: "ScalingMax", "Vector3D", "Vector", "",0,0,0
        P: "DefaultAttributeIndex", "int", "Integer", "",0
        P: "currentUVSet", "KString", "", "U", "map1"
        P: "my_property", "KString", "", "U", "test"
    }
    Shading: T
    Culling: "CullingOff"
}

Mayaのエクスポートプラグイン

SDK

Autodesk FBXから"FBX Extensions Software Development Kit"をダウンロードします。
プラグインは C++ で書きます。詳細はドキュメントとサンプルで確認していただきたいですが、
FBXSDK_DLLEXPORT void MayaExt_ExportEnd(FbxScene* pFbxScene)で出力FBXを加工できそうです。
FbxSceneを作成した後に呼ばれる関数ということですね。

また、DCCツールのプラグイン開発ではツールのバージョンアップに対応する機会が多いため、
ビルド環境を構築する環境をCMakeで作成しておきます。

作成したDLLは以下のディレクトリにコピーします。2016 Extension 2で異なっているので注意します。

2016用: C:\Program Files\Autodesk\Maya2016\bin\plug-ins\FBX
2016.5用: C:\Program Files\Autodesk\Maya2016.5\plug-ins\fbx\plug-ins\FBX

UVアニメーションの検索

まずは、UVアニメーションはどこから取得できるか、Maya上で確認します。
アトリビュートエディタを見るとplace2dTextureにセットされているようです。
作り方によるでしょうが、上手く動かない場合が出たらその都度対応でよいと思います。
アトリビュートエディタ

ハイパーグラフ上では以下のようになっています。メッシュ ⇒ シェーダ ⇒ place2dTextureの順に辿れそうです。
ハイパーグラフ

シェーダがついたメッシュノードを走査するコードは以下になります。
これが一番速い易しいと思います。

{
    for(MItDependencyNodes nodeIterator(MFn::kMesh); !nodeIterator.isDone(); nodeIterator.next()){
        MObject node = nodeIterator.thisNode();
        if(node.isNull()){
            continue;
        }
        if(!node.hasFn(MFn::kDagNode)){
            continue;
        }

        MFnMesh mesh(node);
        MFnDagNode parent(mesh.parent(0));
        parent.getPath(nodePath_);
#ifdef APPDEBUG
        log_ << Log::indent << "path " << nodePath_.fullPathName().asChar() << Log::endl;
#endif

        for(unsigned int i=0; i<mesh.instanceCount(true); ++i){
            MObjectArray shaders;
            MIntArray indices;
            mesh.getConnectedShaders(i, shaders, indices);
            for(unsigned int j=0; j<shaders.length(); ++j){
                MFnDependencyNode shaderNode(shaders[j]);
#ifdef APPDEBUG
                log_ << Log::indent << "traverse shader " << shaderNode.name().asChar() << Log::endl;
#endif
                uuids_.clear();
                traverseConnections(shaderNode);
            }
        }
    }// for(MItDependencyNodes
}

次にマテリアルの属性を辿ります。ハイパーグラフ上の矢印を辿っているイメージです。
OpenMayaでこの辺りの詳細説明を見つけることができませんでした。
参考になるかはわかりませんが、MPlugについては以下の説明をリンクしておきます。
AutoDesk Area Japan 第68回:AttributeとPlugについて語る

ハイパーグラフを見るとわかりますが巡回しているので注意が必要です。
非巡回(DAG)なのはメッシュやスキンのノードツリーのところで、シェーダのところは巡回します。
UUIDで比較していると不具合がでそうですが、現在は問題がでていないのでそのままにしています。

void traverseConnections(const MFnDependencyNode& dst)
{
    //同じuuidが出現したら巡回している
#if 201516<MAYA_API_VERSION
    std::string uuid(dst.uuid().asString().asChar());
#else
    std::string uuid(dst.name().asChar());
#endif
    if(uuids_.end() != uuids_.find(uuid)){
        return;
    }
    uuids_.insert(uuid);
#ifdef APPDEBUG
    log_ << uuid.c_str() <<  Log::endl;
#endif
    Log::ScopeIndent scoped(log_);

    //繋がっているMPlugを全検索
    MPlugArray plugs;
    dst.getConnections(plugs);
    for(unsigned int i=0; i<plugs.length(); ++i){
        MPlugArray connections;
        connections.clear();
        plugs[i].connectedTo(connections, true, false);
        for(unsigned int j=0; j<connections.length(); ++j){
            if(connections[j].node().hasFn(MFn::kDagNode)){
                continue;
            }
            //アニメーション付きのplace2dTextureならアニメーション取得
            if(connections[j].isCompound()){
                for(unsigned int k=0; k<connections[j].numChildren(); ++k){
                    MPlug childPlug = connections[j].child(k);
                    if(MAnimUtil::isAnimated(childPlug)){
#ifdef APPDEBUG
                        log_ << "animated compound: " << childPlug.name().asChar() << Log::endl;
#endif
                        if(connections[j].node().apiType() == MFn::kPlace2dTexture){
                            runPlug(childPlug);
                        }
                    }
                }
            }else if(MAnimUtil::isAnimated(connections[j])){
#ifdef APPDEBUG
                log_ << "animated: " << connections[j].node().apiTypeStr() << Log::endl;
#endif
                if(connections[j].node().apiType() == MFn::kPlace2dTexture){
                    runPlug(connections[j]);
                }
            }
            MFnDependencyNode src(connections[j].node());
            traverseConnections(src);
        }
    }
}

最後に、アニメーションを取得してFBXにカスタムのプロパティを追加します。
プロパティは確実にUnityに渡すことができる文字列にしています。
コードを貼っているだけで申し訳ないですが、 本編ではこれで最後です。

void runPlug(const MPlug& plug)
{
    Log::ScopeIndent scoped(log_);
    MObjectArray animations;
    if(!MAnimUtil::findAnimation(plug, animations)){
        return;
    }
    for(unsigned int i=0; i<animations.length(); ++i){
        MObject animCurveNode = animations[i];
        if(!animCurveNode.hasFn(MFn::kAnimCurve)){
            continue;
        }
        MFnAttribute attribute(plug.attribute());
        if(attribute.name() == "offsetU"){
            writeAnimationCurve(plug, MFnAnimCurve(animCurveNode), "xxx_decal_tu");
        }else if(attribute.name() == "offsetV"){
            writeAnimationCurve(plug, MFnAnimCurve(animCurveNode), "xxx_decal_tv");
        }
    }
}

void writeAnimationCurve(const MPlug& plug, const MFnAnimCurve& animCurve, const char* name)
{
    //----
    //中略
    //----
    // ノードのパス文字列でFbxSceneからFbxNodeを検索する
    FbxNode* node = findNode(nodePath_.fullPathName());
    if(NULL == node){
        return;
    }
    FbxProperty propCurve = FbxProperty::Create(node, FbxStringDT, animName.Buffer());
    propCurve.ModifyFlag(FbxPropertyFlags::eUserDefined, true);
    //----
    //中略
    //----
    propCurve.Set(FbxString(animString.c_str()));
}

Unityのインポートエディタ拡張

Unity側は特に悩むところはないです。Maya側で設定したキー、上の例では"xxx_decal_tu"、"xxx_decal_tv"で
アニメーションが文字列としてインポートできるため、アニメーションを復元し他スキン用のアニメーションと合成すれば完成です。
気を付ける点は、設定する以下4つのシェーダプロパティはセットで設定します。Repeatのアニメーションがなければ適当に追加します。

private const string PropertyNameDecalTexRepeatU = "material._DecalTex_ST.x";
private const string PropertyNameDecalTexRepeatV = "material._DecalTex_ST.y";
private const string PropertyNameDecalTexOffsetU = "material._DecalTex_ST.z";
private const string PropertyNameDecalTexOffsetV = "material._DecalTex_ST.w";

表情をUVアニメーションで付けたかったため、デカールテクスチャです。
何の証明にもなりませんが、以下が復元合成したアニメーションです。
Unityアニメーション

まとめ

駆け足かつコードを貼り付けただけですが、MayaのUVアニメーションをUnityへインポートする方法の説明をしました。
作業自体のまとめは以下です。

  • 良かった点
    • 要望を達成した。
    • 作業期間は概ね予定どおりで終了した。
  • 問題点
    • DLL方式のプラグインの場合、DLLを解放する仕組みがないと開発がつらい。
      • Mayaの再起動に時間がかかる。だから、1日4~5時間作業で、他の仕事と同時進行の見積もり。
  • 改善点
    • Pythonだけで解決できないか。

発展

この記事を書くにあたりコードなどを見返していて、スキニングに削れそうなキーフレームがいくつかあることに気づきました。
現在は頑張って削る必要はないのですが、将来必要に応じてインポート拡張に入れる準備もしておきましょう。
コードだけですが。

public static AnimationCurve decimate(AnimationCurve src, float tolerance=1.0e-4f, int resolution=1024)
{
    System.Diagnostics.Debug.Assert(null != src);
    System.Diagnostics.Debug.Assert(0.0f<=tolerance);
    System.Diagnostics.Debug.Assert(0<resolution);

    AnimationCurve dst = new AnimationCurve(src.keys);
    int srcIndex=1;
    int dstIndex=1;
    float invResolution = 1.0f/resolution;
    for(;;) {
        if(dst.length<3) {
            break;
        }
        if((src.length-1)<=srcIndex) {
            break;
        }
        //キーを削除しても、セクションが似ているかチェック
        Keyframe key = dst[dstIndex];
        dst.RemoveKey(dstIndex);

        float start = src[srcIndex-1].time;
        float end = src[srcIndex+1].time;
        float duration = end - start;
        bool equal = true;
        if(1.0e-5f<duration) {
            for(int i = 1; i<resolution; ++i) {
                float time = i*invResolution*duration + start;
                float v0 = dst.Evaluate(time);
                float v1 = src.Evaluate(time);
                if(tolerance<Mathf.Abs(v0-v1)) {
                    equal = false;
                    break;
                }
            }
        }
        //似ていなかったらキーを元に戻す
        if(!equal) {
            dst.AddKey(key);
            ++dstIndex;
        }
        ++srcIndex;
    }
    return dst;
}

おわりに

いかがでしたでしょうか、何かしら楽しげな仕事風景を、というつもりがただコードのコピーになってしまいました。
私のように、コードを示された方が理解が進む人がたくさんいることを願います。
プロジェクト初期はこのような作業環境準備の仕事がほとんどです。
将来チームが楽をするためにしっかりと土台を作っておきたいものです。

私達、第三研究開発本部では、このような下支えの仕事に積極的な方、もちろんゲーム作りが好きな方と
一緒に働きたいと考えています。もしご興味を持たれましたら、弊社グループ採用サイトをご確認ください。

採用情報|株式会社セガ・インタラクティブ
採用情報|株式会社セガゲームス -【SEGA Games Co., Ltd.】

Powered by はてなブログ