Autodesk University Japan 2017Autodesk University Japan 2017

チュートリアル / Mayaにゲームエンジンを組み込んでみよう!~効率的なアセット製作環境を目指して~
第3回:レンダラをカスタマイズしよう!

2016.09.13

  • Maya
  • ゲーム
  • スクリプト・API
  • チュートリアル
  • 上級者
ガンバリオン

みなさん、こんにちは。ガンバリオンの森下です。前回はプラグインの開発環境とビルド設定について見てきました。今回からは内部処理の詳細について見ていきたいと思います。実装の詳細に関しては前回紹介した公開中のインターフェースプラグインに沿って行っていきますので、前回の記事を参考にソースコードを確認できる環境を手元にご用意ください。

実装の概要

Mayaにゲームエンジンを組み込むにあたって必要なことは大きく分けて2つです。それは、

・Mayaのビューパネル上に自由に描画できるようにする(レンダラをカスタマイズする)
・Mayaから描画に必要なデータ(頂点データ、マテリアル等)を取得し、変更に追従する

です。

1つ目については、当然ですが何もしなければMayaはデフォルトレンダラを使用してメッシュ等を描画しますので、レンダラをカスタマイズしてデフォルトの描画処理をオフにし、代わりにゲームエンジンを使用して描画するように差し替えてやる必要があります。
2つ目については、いくらレンダラをカスタマイズしても描画するデータがわからなければゲームエンジン側では何も描画することができません。逆にいえばこれらデータを取得できさえすれば、描画に関しては実際のゲームと何ら変わりなく行うことができます。データを取得せず、オブジェクトの描画をMayaのシステムに任せたまま描画パスを増やす方法もあるにはあるのですが、融通が利きませんし、新たに描画システムを構築しなければなりません。その点描画データをゲームエンジン側に渡すことができれば、それをゲーム同様の描画オブジェクトとして処理してやることでゲームエンジンを改変することなくMaya上に描画を行うことができます。
2つ目に関しての詳しい解説は次回に回して、今回は1つ目について解説していきたいと思います。

レンダラのカスタマイズの概要

レンダラのカスタマイズ方法を説明していくにあたって、全体像を簡単に説明しておきたいと思います。
Mayaの描画をカスタマイズするためには、カスタム処理を実行するクラスを作成し、Mayaに登録します。Maya APIではその雛形となるMRenderOverrideというクラスが用意されていて、このクラスを継承し、仮想関数をオーバーライドすることで描画をカスタマイズします。このクラス内では「オペレーション」という単位で描画処理を定義していきます。このオペレーションはMayaにおける描画パスのようなもので、MayaのレンダラはMRenderOverrideの関数を通じてオペレーションを取得し、描画を実行していきます。結局のところMRenderOverrideの実態はMayaのレンダラにオペレーションを渡すためのイテレータで、単純に描画の流れを定義するものだと思ってもらえればよいかと思います。
オペレーションもMRenderOperationというクラスが用意されているので、これを継承することでカスタマイズを行うことができます。実際のゲームエンジンの描画処理は1つのオペレーションとして実装されます。

なんとなく理解できたでしょうか?カスタマイズに必要なことをまとめると、

・MRenderOverrideに描画の流れを定義する
・MRenderOperationに必要な描画処理を記述する

になります。では、それぞれ実際のソースと共に見ていきます。

MRenderOverrideのカスタマイズ

MRenderOverrideを継承しているクラスはCustomRenderOverride.h/.cppに定義されているCustomRenderOverrideになります。このクラスをMayaに登録します。該当の個所は、CustomViewportMain.cppのinitializePlugin関数です。

MStatus initializePlugin(MObject obj)
{
	/* 省略 */

	// レンダラ
	MHWRender::MRenderer* renderer = MHWRender::MRenderer::theRenderer();
	if (renderer) {
		if (!renderOverrideInstance) {
			renderOverrideInstance = new CustomRenderOverride("customViewport");
			renderer->registerOverride(renderOverrideInstance);
		}
	}

	/* 省略 */
	return status;
}

initializePlugin関数はプラグインがロードされた際に自動的に1度呼び出されますので、その中で登録しておけば大丈夫です。登録はMHWRenderer::MRenderer::theRenderer()で取得できるMHWRenderer::MRendererに対してregisterOverrideでCustomRenderOverrideのインスタンスを登録します。ちなみにプラグインがアンロードされる場合にはuninitializePluginが呼び出されますので、その中では逆にMHWRenderer::MRenderer::deregisterOverride()で登録の解除を行っておきましょう。
これでレンダラをカスタマイズする準備ができました。続いてCustomRenderOverrideの内部を見ていきます。

概要でも述べた通り、MRenderOverrideの実態はオペレーションのイテレータです。内部にオペレーションの配列を持たせ、順番にMayaレンダラに渡すために関数をオーバーライドしていきます。オーバーライドが必要な関数は以下になります。

virtual bool startOperationIterator();
virtual MHWRender::MRenderOperation* renderOperation();
virtual bool nextRenderOperation();
virtual MStatus setup(const MString& destination);
virtual MStatus cleanup();

これらの関数がMayaレンダラから実際にどのような呼び出され方をするかを疑似コードで表すと、

/* Mayaレンダラのレンダリング処理の疑似コード */
void MayaRendering(MHWRender::MRenderOverride* o)
{
	o->setup(); // 描画準備
	if (o->startOperationIterator()) { // イテレータリセット
		do {
			// オペレーションの取得
			MHWRender::MRenderOperation* operation = o->renderOperation();
			/* オペレーションの実行 */
		} while (o->nextRenderOperation()); // イテレータを進める
	}
	o->cleanup(); // 後始末
}

となります。名前から想定される通りの呼び出され方だと思いますので、理解しやすいのではないでしょうか。renderOperation()が返す順番通りにMayaレンダラはオペレーションを実行していきます。

setup()での描画準備は主にレンダーターゲットの準備を行います。プラグイン内ではある程度自由にレンダーターゲットを作ることができますが、今回はゲームエンジンでの描画結果を出力するためのカラーバッファと、デプスバッファをそれぞれ一枚ずつ作成しています。下準備としてCustomRenderOverrideのコンストラクタ内で、

// カラーバッファ
targetOverrideNames_[kColor] = MString("__CustomRenderOverride_Color_Target1__");
targetDescriptions_[kColor] = new MHWRender::MRenderTargetDescription(targetOverrideNames_[kColor], 256, 256, 1, MHWRender::kR8G8B8A8_UNORM, 0, false);
targets_[kColor] = nullptr;
// デプスバッファ
targetOverrideNames_[kDepth] = MString("__CustomRenderOverride_Depth_Target__");
targetDescriptions_[kDepth] = new MHWRender::MRenderTargetDescription(targetOverrideNames_[kDepth], 256, 256, 1, MHWRender::kD32_FLOAT, 0, false);
targets_[kDepth] = nullptr;

のように、MHWRender::MRenderTargetDescriptionを生成しています。
レンダーターゲットに渡す名前は被らなければ問題ありません。ここではまだDescriptionのベースを作っているのみで、実際にはまだレンダーターゲットを作成していません。実際に作成するのはsetup()内になります。Mayaのビューパネルはユーザーが自由にリサイズを行うことができます。そのため、サイズが確定し描画される直前に呼び出されるsetup()でレンダーターゲットの生成、リサイズを行うのが都合がいいのです。setup()内ではupdateRenderTargets()が呼び出され、この関数内でレンダーターゲットの生成等が行われます。実際どうなっているかというと、

MStatus CustomRenderOverride::updateRenderTargets(MHWRender::MRenderer* theRenderer, const MHWRender::MRenderTargetManager* targetManager)
{
	/* 省略 */

	// 現在のパネルのサイズを取得
	uint32_t targetWidth, targetHeight;
	theRenderer->outputTargetSize(targetWidth, targetHeight);

	// ターゲットのリサイズ
	for (uint32_t targetId = 0; targetId < kTargetCount; ++targetId) {
		targetDescriptions_[targetId]->setWidth(targetWidth);
		targetDescriptions_[targetId]->setHeight(targetHeight);
		if (!targets_[targetId]) {
			targets_[targetId] = targetManager->acquireRenderTarget(*targetDescriptions_[targetId]);
		} else {
			targets_[targetId]->updateDescription(*targetDescriptions_[targetId]);
		}
	}

	/* 省略 */
}

現在のパネルのサイズを取得し、それをもとにMHWRender::MRenderTargetManagerを使用してターゲットを生成、更新を行っています。

その他の関数に関してはオペレーションの配列のインデックスをリセット、インクリメント、最大値を超えたらfalseを返す、というだけのものですのでソースを見ていただければと思います。

CustomRenderOverride内では他にもゲームエンジンの初期化等も行っています。ゲームエンジンで描画を行うためにはDirectXのデバイスが必要となりますが、こちらは

ID3D11Device* dxDevice = (ID3D11Device*) MHWRender::MRenderer::theRenderer()->GPUDeviceHandle();

で取得することができますので、自分のゲームエンジンを組み込む際はこれを渡してあげれば問題ないかと思います。

MRenderOperationのカスタマイズ

続いてオペレーション用のクラスであるMRenderOperationを見ていきます。
MRenderOperationはオペレーションの基底クラスで、MayaAPIにはこれを継承したMayaの描画のテンプレートとなる機能を持ったクラスがいくつか公開されています。主だったオペレーションの種類は以下のようになります。

MUserRenderOperationユーザが自由にカスタム可能
MSceneRenderMayaシーン描画パス
オブジェクト、MayaUI(ワイヤーフレーム等)
MHUDRenderHUD描画パス
MQuadRenderスクリーン全体に対して描画
MPresentTargetMayaバックバッファをビューポートに表示

これらをさらにユーザーが継承して簡単にカスタマイズできるようになっているのです。
実際にこれらを継承したクラスがCustomRenderOperation.hに定義されています。Mayaレンダラに処理される順番に挙げてみると、

・CustomRenderOperation : MUserRenderOperation
・UIItemRenderOperation : MSceneRender
・HUDRenderOperation : MHUDRender
・ToBackBufferOperation : MQuadRender
・PresentOperation : MPresentTarget

となります。一番重要なものはCustomRenderOperationで、このオペレーションでゲームエンジンの描画を行います。CustomRenderOperationはMUserRenderOperationを継承しています。MUserRenderOperationはexecute()関数をオーバーライドすることでユーザが自由に描画処理を記述できるオペレーションです。すでにDirectXのデバイスも取得できており、デバイスからコンテキストも取得できるため、execute()内では自由に描画を行うことが可能ですが、最終的な描画結果をMayaのレンダーターゲットに出力してあげる必要があります。

CustomRenderOverrideで作成したレンダーターゲットが、CustomRenderOverride::updateRenderTarget()でCustomRenderOperation::setRenderTargets()を通じてオペレーションのほうに渡されていて、このターゲットに対して出力します。MayaのレンダーターゲットはMRenderTargetというクラスですが、このクラスが持つresourceHandle()関数が返すvoidポインタが実はDirectXのオブジェクトになっており、カラーバッファの場合ID3D11RenderTargetView*、デプスバッファの場合はID3D11DepthStencilView*が返ってきますので、それをそのまま以下の通り使用可能です。

colorBuffer.CreateFromRTV(static_cast<ID3D11RenderTargetView*>(targets_[0]->resourceHandle()));
depthBuffer.CreateFromDSV(static_cast<ID3D11DepthStencilView*>(targets_[1]->resourceHandle()));

あとはゲームエンジンの描画をこのexecute()内で行うだけです。これでMaya上にゲームエンジンを使用した描画ができました。しかし、このオペレーションだけではワイヤーフレームやマニピュレータなどが一切表示されません。これを描画するのが次のUIItemRenderOperationです。

UIItemRenderOperationはMSceneRenderを継承しています。MSceneRenderは通常シーンの描画をつかさどるオペレーションで、シーン上のオブジェクトをシェーディングノードに基づいて描画したり、ワイヤーフレームやマニピュレータなどのUIまで描画してくれます。今回の場合は、ゲームエンジンを使用してオブジェクトの描画はすでに完了しているため、オブジェクトの描画はスキップして、UIのみを描画します。これはrenderFilterOverride()をオーバーライドします。

virtual MHWRender::MSceneRender::MSceneFilterOption renderFilterOverride() override
{
	return MHWRender::MSceneRender::kRenderUIItems;
}

この関数ではMSceneFilterOpitionに定義されている項目を描画したい項目のフラグとして返すことで描画をフィルタリングすることができます。MSceneFilterOpitionには様々な種類がありますが、ここではUIのみを描画したいためkRenderUIItemsのみを指定して返しています。あとはこのオペレーションでも先ほどのCustomRenderOperationで描画したレンダーターゲットに対して描画を行う必要があるため設定を行います。これはtargetOverrideList()をオーバーライドします。

virtual MayaRenderTargets targetOverrideList(unsigned int& listSize) override {
	if (targets_) {
		listSize = targetNum_;
		return targets_;
	}
	return nullptr;
}

引数で渡されているlistSizeにレンダーターゲットの数を渡します。デプスバッファもターゲットのうちの一つと扱われますので、今回の場合だと2を入れる必要があるので注意してください。戻り値としてMRenderTargetの配列を返しています。このMRenderTargetが描画の際に使用されます。

これでUIまで描画できました。次はHUDです。HUDはメニューのヘッドアップディスプレイから選択したときに表示される文字等を表します。HUDRenderOperationがその役割を担いますが、こちらに関してはUIItemRenderOperationと同様にレンダーターゲットの設定を行います。これだけでHUDが表示されるようになります。

ここまでで表示したいもの自体はすべて描画することができました。では残りの2つで何をしているかというと、ToBackBufferOperationはここまで描画してきたレンダーターゲットをMayaのバックバッファにコピーしています。CustomRenderOverrideで作成したMRenderTargetはあくまで独立したレンダーターゲットで、Mayaが最終的にビューパネルに表示する際に使用されるバックバッファにはまだ何も書かれていない状態ですので、このオペレーションでコピーを行っています。
PresentOperationはDirectXのPresentと同じようなものでこのオペレーションを呼ぶことでバックバッファがスワップされます。

長くなりましたが、これでレンダラのカスタマイズについての解説は終わりです。解説しきれていない部分もありますが、ソースのコメントにも細かく記載していますので、参考にしていただき補完していただければと思います。さて、次回はMayaから描画に必要なデータを取得するための仕組みを解説したいと思います。ぜひお付き合いください!

著者

森下 宏樹

森下 宏樹

株式会社ガンバリオン
テクニカルチーム エンジニア