チュートリアル / Mayaにゲームエンジンを組み込んでみよう!~効率的なアセット製作環境を目指して~
第4回:Mayaとゲームエンジンのインターフェースを作ろう
- Maya
- ゲーム
- コラム
- スクリプト・API
- チュートリアル
- 上級者
みなさん、こんにちは。ガンバリオンの森下です。前回はMayaのレンダラのカスタマイズについて見てきましたが、今回はMayaから描画に必要となるデータを取得し、ゲームエンジンに渡すインターフェース部分について見ていきたいと思います。今回もGitHubにて公開中のインターフェースプラグイン(MayaGameEngineInterface)に沿って解説を行いますので、ソースコードを確認できる環境を手元にご用意ください。
連載は今回で最後となります。小難しい話が続きますが、ぜひ最後までお付き合いください!
インターフェースの概要
前回まででMayaのレンダラをカスタマイズし、自由な描画を可能とするところまでを解説しました。実際にゲームエンジンを使用して描画するためには、頂点データ、インデックスデータ、テクスチャデータなどをMayaから取得し、これをもとにDirectXのリソースオブジェクトをゲームエンジン側で生成して描画コールを発行する必要があります。またMayaから取得したデータはMaya上でのユーザーの操作によって随時変更されますので、常時変更を監視し、ゲームエンジン側に反映しなければなりません。これらの要件を満たすインターフェースとして、MayaGameEngineInterfaceでは、Mayaの各DAGノードを簡易的にミラーリングするオブジェクトをそれぞれ生成することで実装しています。ここでは便宜上MirrorDAGとします。このMirrorDAGは第1回でも紹介した「Killzone Shadow Fall: Creating art tools for a new generation of games」で提案されている方法で、非常にすっきりとした形でインターフェースを実装することができます。
どのような仕組みになっているかを簡単に説明しておきます。 transformやmesh等の描画に必要なデータを持つMayaのDAGノードが生成されると、同時にMirrorDAG側でも対応するMirrorDAGノードが生成されます。MirrorDAGノードは生成されると、対応するMayaDAGノードのアトリビュートの変更の監視をし始め、合わせてゲームエンジン側で必要なデータをノードのアトリビュートから取得し、ゲームエンジンに渡します。また、アトリビュートの変更を検知したタイミングでも更新されたデータをゲームエンジン側に渡します。ゲームエンジンはMayaDAGに直接アクセスすることはなく、すべてMirrorDAGを経由してデータのやり取りを行います。MirrorDAGにはMayaのデータをゲームエンジンに合わせた形のデータに変換して渡すような処理が記述されているので、ゲームエンジン側はMaya用に特別な処理を書く必要がありません。こうすることでゲームエンジンとMayaAPIとの依存関係を排除することができ、シンプルな構造でプラグインの実装を行うことができます。
インターフェース部分の概要は以上になります。続いて、MayaGameEngineInterfaceではどのように実装されているかの詳細を見ていきたいと思います。
MayaDAGノードの生成・削除の監視
各MayaDAGノードに対応するMirrorDAGノードを生成するためには、Maya上で行われるMayaDAGノードの生成と削除を監視する必要があります。これはDAGManagerクラスで行われています。DAGManagerクラスはMirrorDAGノードを管理するクラスで、各MirrorDAGノードはDAGManagerを介して生成や削除、また更新処理や、描画処理などを行います。監視を開始する処理はDAGManager.cppのDAGManagerコンストラクタ内で行われています。
DAGManager::DAGManager() { settings_ = nullptr; MStatus status; // MayaDAGの生成を監視 addNodeCallBackId_[CallBack_Transform] = MDGMessage::addNodeAddedCallback(AddNodeCallback, "transform", nullptr, &status); if (!status) MDisplayError("[MayaCustomViewport] / Failed addNodeAddedCallback"); // MayaDAGの削除を監視 removeNodeCallBackId_[CallBack_Transform] = MDGMessage::addNodeRemovedCallback(RemoveNodeCallback, "transform", &transformList_, &status); if (!status) MDisplayError("[MayaCustomViewport] / Failed addNodeRemovedCallback"); /* 省略 */ }
MayaDAGノードの生成、削除の監視はMDGMessage::addNodeAdded/RemovedCallback関数にコールバックを登録することで行うことができます。第2引数の”transform”は監視したいノードの種類を表しています。メッシュノードであれば”mesh”とします。ここでは長くなるため省略していますが、必要な種類分すべて監視するようコールバックの登録を行います。登録している関数はDAGManager::AddNodeCallback/RemoveNodeCallbackになります。
void DAGManager::AddNodeCallback(MObject& node, void* clientData) { MFnDependencyNode nodeFn(node); DAGNode* dagNode = nullptr; // 独自DAGを生成する switch (node.apiType()) { case MFn::kTransform: dagNode = new DAGTransform(node); instance_->transformList_.push_back(dagNode); break; /* 省略 */ } if (dagNode) { instance_->nodeMap_[MObjectHandle(node)] = dagNode; } } void DAGManager::RemoveNodeCallback(MObject& node, void *clientData) { // 独自DAG破棄 if (clientData) { DAGNodeList& list = *reinterpret_cast<DAGNodeList*>(clientData); auto iter = list.begin(); while (iter != list.end()) { DAGNode* curNode = *iter; if (*curNode == node) { delete curNode; list.erase(iter); break; } iter++; } } /* 省略 */ MObjectHandle handle(node); auto iter = instance_->nodeMap_.find(handle); if (iter != instance_->nodeMap_.end()) { instance_->nodeMap_.erase(iter); } }
AddNodeCallbackでは引数に生成されたMayaのノードのMObjectが渡されますので、このMObjectの種類をMFnDependancyNode::apiType関数で調べ、その種類に応じたMirrorDAGノードを生成しています。RemoveNodeCallbackはその逆の処理で、渡されたMObjectをもとにDAGManager上で管理しているMirrorDAGノードの中から対応するものを検索し、削除を行っています。
ここまででMayaDAGノードに対応するMirrorDAGノードを生成、削除することができました。続いてノードのアトリビュートを監視する方法を見ていきます。
MayaDAGノードのアトリビュートの監視
アトリビュートの監視処理はDAGNodeクラスで実装されています。このDAGNodeクラスは各種MirrorDAGノードの基底クラスで、ここで監視を開始する処理を行い、継承先のクラスはDAGNodeのAttributeChanged関数をオーバーライドすることで対応するノードのアトリビュートの変更を検出できるような仕組みになっています。 アトリビュートの監視開始処理はDAGNodeのコンストラクタ内で実装されています。
DAGNode::DAGNode(MObject& object, bool useDirtyCallback) { handle_ = object; // アトリビュートの変更時のコールバック登録 MStatus status; attrChangeCallBackId_ = MNodeMessage::addAttributeChangedCallback(object, AttributeChangeCallcack, this, &status); /* 省略 */ }
MNodeMessage::addAttributeChangedCallback関数に監視したいノードのMObjectを渡してあげることでアトリビュートの監視は実現できます。コールバックとしてDAGNode::AttributeChangeCallback関数を登録していますが、この内部で先ほど出てきたAttributeChangedを呼び出すことで各種継承先に通知が行くようになっています。このDAGNode::AttributeChanged関数の定義は
virtual void AttributeChanged(MNodeMessage::AttributeMessage msg, MPlug& plug, MPlug& otherPlug)
となっており、引数のmsgには変更の種類、plugには変更があったアトリビュートの値アクセス用のプラグ、otherPlugにはmsgがアトリビュートのコネクションに関連するものだった場合にコネクション相手のプラグが渡されるようになっています。
通常のアトリビュートの値の変更の場合はplugからアトリビュートの名前と、変更された値を取得できます。ノードの位置情報を持つtransformノードに対応するDAGTransformの場合を例に見てみます。
void DAGTransform::AttributeChanged(MNodeMessage::AttributeMessage msg, MPlug& plug, MPlug& otherPlug) { MStatus s; if (msg & MNodeMessage::kAttributeSet) { MFnAttribute fnAttr(plug.attribute()); MString sn = fnAttr.shortName(); // アトリビュートのショートネームを取得 if (sn == "t") { // trans GetVectorByPlug(position_.ToFloatArray(), plug); } else if (sn == "tx") { // trans.x position_.x = plug.asFloat(); } /* 省略 */ } }
値の変更があった場合msgにはMNodeMessage::kAttributeSetというフラグが渡されています。このフラグが立っている場合はplugでアクセスできるアトリビュートの値が変更されたということになります。その場合アトリビュートの名前からどのデータが更新されたかを識別して、値を取得することでMirrorDAGノード側でMayaDAGノードの値をミラーリングできます。
また、アトリビュートに対するコネクションが成立した場合もこのコールバックが呼び出されます。こちらは頂点データを持つmeshノードに対応するDAGMeshを例に見てみます。
void DAGMesh::AttributeChanged(MNodeMessage::AttributeMessage msg, MPlug& plug, MPlug& otherPlug) { // マテリアルとの接続チェック MStatus status; if (msg & MNodeMessage::kConnectionMade) { // コネクション生成時 auto obj = otherPlug.attribute(&status); if (status) { MFnAttribute otherAttr(obj); if (otherPlug.node().apiType() == MFn::kShadingEngine) { MString attrName = otherAttr.name(); if (otherAttr.name() == "dagSetMembers") { DAGNode* dagMaterial = DAGManager::Get()->FindDAGNode(otherPlug.node()); if (dagMaterial) { Connect(dagMaterial); } } } } } else if (msg & MNodeMessage::kConnectionBroken) { // コネクション破棄時 /* 省略 */ } updated_ = true; }
コネクションの生成、破棄が行われた場合、msgにはMNodeMessage::kConnectionMade/Brokenフラグが渡されています。meshノードは生成されると描画を行うために必要なshadingEngineノードとコネクションされます。shadingEngineはMirrorDAG上ではマテリアルのデータをミラーリングするDAGMaterialとして管理されており、DAGMeshの頂点データを使用して描画を行う際には、自身にコネクションされたshadingEngineに対応するDAGMaterialのデータを使用するため、ここでotherPlugからコネクションされるshadingEngineを取得し、DAGManager経由で対応するDAGMaterialを取得しています。コネクションが破棄された場合は生成時と同様にDAGMaterialを取得し、DAGMeshが保持しているマテリアルリストから削除するだけなのでここではソースは省略しています。
MayaDAGノードの階層構造の監視
MayaDAGのtransformノードは階層構造を持っています。transformノードの子にtransformノードを持つことができますし、mesh等のシェイプノードは基本的に位置情報を持っておらず、transformノードにぶら下がることでワールド空間に存在しています。そのためMirrorDAGでもこの階層構造を監視し、ミラーリングしてやります。この監視もコールバックで実現できます。
DAGTransform::DAGTransform(MObject& object) { MDagPath path; MFnDagNode(object).getPath(path); // DAG監視用のコールバックを登録 dagAddedChildCallback_ = MDagMessage::addChildAddedDagPathCallback(path, [](MDagPath& child, MDagPath& parent, void* clientData) { DAGTransform* own = reinterpret_cast<DAGTransform*>(clientData); DAGNode* node = DAGManager::Get()->FindDAGNode(child.node()); if (node) own->AddChild(node); }, this); dagRemovedChildCallback_ = MDagMessage::addChildRemovedDagPathCallback(path, [](MDagPath& child, MDagPath& parent, void* clientData) { DAGTransform* own = reinterpret_cast<DAGTransform*>(clientData); DAGNode* node = DAGManager::Get()->FindDAGNode(child.node()); if (node) own->RemoveChild(node); }, this); }
MDagMessage::addChildAdded/RemovedDagPathCallbackに登録します。ここではラムダを使用して実装しています。階層構造に変化があった場合これらのコールバックが呼び出されるので、childに渡される子のオブジェクトから対応するMirrorDAGノードを取得して階層構造をMirrorDAG側でも構築しています。これで監視自体はできるのですがノードの生成時に問題が発生します。polySphereなどでmeshノードを生成した場合、自動的にtransformノードも生成されるのですが、その際に階層構造が構築され、このコールバックが呼ばれた際にまだDAGManagerにDAGMeshが登録されておらず、正常に階層構造を構築できません。そのためDAGTransform::Update関数が最初に呼ばれた際に一度階層構造をチェックしています。
void DAGTransform::Update() { MFnDagNode dagFn(handle_.objectRef()); if (!initialized_) { for (uint32_t i = 0; i < dagFn.childCount(); i++) { DAGNode* node = DAGManager::Get()->FindDAGNode(dagFn.child(i)); if (node) { AddChild(node); } } initialized_ = true; } /* 省略 */ }
MFnDagNode::child関数で子のノードを取得できますので、このノードに対応するMirrorDAGノードを取得し、子として登録しています。
ここまででMirrorDAGによるMayaDAGのミラーリング方法の解説は一通り終わりましたが、DAGMeshで行っている頂点データの取得部分が少し複雑ですので解説しておきたいと思います。
Mayaから頂点データを取得する方法
頂点データを取得している部分はDAGMesh::UpdateGeometory関数です。この関数を順を追って解説していきます。
各meshはshadingEngineとリンクされ、ゲームエンジン側で描画を行う際にはこのshadingEngine応じてマテリアルを切り替える必要があります。meshにリンクされているshadingEngineは必ずしも1つとは限らず、meshの各面で違う可能性があります。なのでまずはリンクされているシェーダのリストと、各面にどのシェーダが割り当てられているかのリストを取得します。
MStatus status; MFnMesh mesh(handle_.objectRef(), &status); // ポリゴン各面にアサインされているシェーダリストを取得する MObjectArray shaders; MIntArray shaderIndices; shaders.clear(); shaderIndices.clear(); if (!mesh.getConnectedShaders(0, shaders, shaderIndices)) { return; }
meshに関する情報はMFnMeshで取得することができます。ここではmeshのオブジェクトからMFnMeshを生成し、MFnMesh::getConnectedShaders関数によって情報を取得しています。MFnMesh::getConnectedShaders関数では第2引数にリンクされているshadingEngineのオブジェクトのリスト、第3引数に各面に使用しているshadingEngineの第2引数で取得したリスト上のインデックスが返されます。 各面をどのshadingEngineで描画すればいいかは取得できたので、今度はこの情報を元に頂点、インデックス情報を取得していきます。これらのデータはMHWRender::MGeometryExtractorを使用して取得することができますが、取得するためにはMHWRender::MGeometryRequirementsの設定が必要になります。
MHWRender::MGeometryRequirements requirements; // index MFnSingleIndexedComponent comp; MObject compObj = comp.create(MFn::kMeshPolygonComponent); comp.addElements(shadingPolyIds); // IDによる制御 MHWRender::MIndexBufferDescriptor triangleDesc(MHWRender::MIndexBufferDescriptor::kTriangle, "", MHWRender::MGeometry::kTriangles, 3, compObj); requirements.addIndexingRequirement(triangleDesc); // vertex MHWRender::MVertexBufferDescriptor posDesc("", MHWRender::MGeometry::kPosition, MHWRender::MGeometry::kFloat, 3); requirements.addVertexRequirement(posDesc); /* uv, normal等の他の要素も設定が必要 */
MHWRender::MGeometryRequirementsにはどのような頂点、インデックス情報を取得したいかをDescriptorを使用して定義していきます。インデックスの場合はMHWRender::MIndexBufferDescriptorで、先ほど取得した各面のshadingEngineのインデックスのリストからMFnSingleIndexedComponentを作り、これを渡してあげることで一つのshadingEngineを使用して描画するためのインデックスリストを取得するという定義を行うことができます。MFnSingleIndexedComponentを渡さなくても取得を行うことはできますが、その際はこのmesh全体を描画するためのインデックスリストを取得する、という定義になります。
頂点データの場合はMHWRender::MVertexBufferDescriptorを使用します。第1引数にはここでは空文字列を指定していますが、カラーや、UVの場合はmeshが複数のセットを持っている可能性があるため、それを識別するためcolorSetやuvSetの名前を指定してあげます。 設定ができたら後はデータを取得するだけです。
MHWRender::MGeometryExtractor extractor(requirements, dagPath, true, &status); if (!status) return; uint32_t numVertices = extractor.vertexCount(); uint32_t numTriangles = extractor.primitiveCount(triangleDesc); uint32_t minBufferSize = extractor.minimumBufferSize(numTriangles, triangleDesc.primitive()); std::unique_ptr<uint32_t[]> triangleIdx(new uint32_t[minBufferSize]); std::unique_ptr<float[]> vertices(new float[numVertices * posDesc.stride()]); vb.resize(requirements.vertexRequirements().length()); if (!extractor.populateIndexBuffer(triangleIdx.get(), numTriangles, triangleDesc)) { return; } if (!extractor.populateVertexBuffer(vertices.get(), numVertices, posDesc)) { return; }
MHWRender::MGeometryExtractorにMHWRender::MGeometryRequirementsを渡して、populateIndexBuffer、populateVertexBufferを使用してデータを取得します。この取得したデータを使用してDirectXの頂点リソース等を生成してあげればメッシュの描画を行うことが可能になります。
レンダラの設定画面
ゲームエンジンを組み込んだ場合、ゲームエンジン側の機能であるポストエフェクト等のパラメータをMaya上で調整ができたほうが便利です。ここではその方法について簡単に説明します。 Mayaではレンダラ切り替えメニューにオプションボタンを表示させることが可能になっています。図の赤丸の部分ですね。
ただレンダラを登録するだけではこのボタンは表示されません。あらかじめMayaにmelの関数を登録しておく必要があります。この役割を果たしているのが、第2回でscriptsフォルダにコピーしたスクリプトファイルcustomViewportOptionBox.melになります。このファイル内ではcustomViewportOptionBox()という関数を定義しています。Mayaでは、[レンダラ名]OptionBox()という関数をあらかじめ定義しておくと、指定した名前のレンダラが登録されたときに先ほどのレンダラ切り替えメニューにオプションボタンが自動的に表示され、ボタンが押されるとこの関数が呼ばれる仕様となっています。ここで言うレンダラ名は第3回で解説したMRenderOverrideのコンストラクタに渡している文字列になるので必ずしもメニューに表示されている名前とは限らないことに注意してください。
このcustomViewportOptionBox()が何を行っているかというと、ウインドウを生成して内部にcustomViewportGlobalsノードのアトリビュートエディタを展開しています。
このcustomViewportGlobalsノードは自作のカスタムノードで、ゲームエンジン側のパラメータをMaya上で保持するためのノードになります。Maya側はゲームエンジン固有のパラメータを知らないので、カスタムノードのアトリビュートとしてパラメータを定義してやることでMaya上での編集を可能にしています。またMayaのシーンデータに保存されるので便利です。アトリビュート自体は簡単に定義してやることができますので、CustomViewportGlobals.cpp/.hを参考にしてもらえればと思います。
さて、今回で「Mayaにゲームエンジンを組み込んでみよう!」のコラムは最終回となります。Mayaのプラグイン、特に自作レンダラの部分に関してはあまり情報がなく、私もかなり試行錯誤して開発を行いました。このコラムや、公開したソースコードが少しでも皆様のお役に立てれば幸いです。それでは、最後までお読みいただきありがとうございました!