Cygames Tech Fes フォローアップ: Unity+スマホで3Dゲーム開発最適化するための考え方


みなさん、こんにちは!
先日の“Cygames Tech Fes”での「Unity+スマホで3Dゲーム開発最適化するための考え方」の資料を公開と、講演の中で紹介したまゆげデモについての紹介記事です。

セッションの中で描画順制御の一例としてUnityちゃんに「まゆげ」をつけたデモを紹介しました。
こちらについて、掘り下げてフォローしていきたいと思います。

© UTJ/UCL

なぜ必要なの?

普通、まゆげは髪の毛の下に存在しており、髪の毛に隠れるはずです。

しかしアニメなどではキャラクター表現の一環として髪の毛より手前に表示されている事が多々あります。

実は、Unityでこれを実現する手段がいくつかあります。
まずはUnity上で動作するサンプルプロジェクトを用意しました。

https://github.com/cygames/MayugeSample

中身について、説明をしていきたいと思います。

シェーダーでの Offset 指定

Offset はその名の通り、Z 値にオフセットをかける機能です。
https://docs.unity3d.com/ja/current/Manual/SL-CullAndDepth.html

機能的には glPolygonOffset とほぼ同じです。以下が参考になります。
https://miffysora.wikidot.com/glpolygonoffset

まゆげにOffset値を設定し、髪の毛よりも手前に表示する、という算段です。
このOffsetはシェーダーを用意するだけで実現可能で、シェーダー中で以下の指定を行います。

	Pass
	{
		Offset -100, -200
		//略
	}

しかしOffset指定では角度によっては問題が発生します。
以下のように、まゆげの一部が手前に出てこないことがあるのです。

© UTJ/UCL

Offset指定はポリゴンの角度に応じて手前に出す量を調整できますが、その値が不足すると「まゆげ」が手前に表示されません。
かと言って極端な値を設定すると、例えばモーションによっては「手」より手前に「まゆげ」が表示されるなどし、破たんしてしまいます。

sortingOrder による描画順制御

Renderコンポーネントが持つsortingOrderとシェーダーが持つZWrite/ZTtestを組みわせることで、Offsetが持つ問題を解決することができます。

sortingOrderはRendererコンポーネントが持つ機能で、描画順番を細かく制御することができます。派生クラスである、MeshRendererやSkinnedMeshRendererでも利用することが可能です。
https://docs.unity3d.com/ja/current/ScriptReference/Renderer-sortingOrder.html

ZWrite/ZTestはシェーダーが持つ機能で、Z値の書き込み、比較方法を制御することができます。
https://docs.unity3d.com/ja/current/Manual/SL-CullAndDepth.html

この手段では、まゆげのZTestを無効化し、髪の毛の手前に描画します。ただその際、まゆげのZ値を書き込むと他のMeshと干渉する可能性があるため、ZWriteを無効化しておきます。また、単純にZTestを無効化するとどのMeshに対してもまゆげが手前に描画されてしまうため、キャラクターを奥から手前へと描画していきます。
従ってこの手段の実現には、sortingOrderを操作するスクリプトと、ZWrite/ZTestを制御するためのシェーダーが必要となります。

© UTJ/UCL

sortingOrderの制御は、以下のように行います。

int characterOrder = 100000 + (int)(Camera.main.worldToCameraMatrix.MultiplyPoint(transform.position).z * 10000);
MeshRenderer[] meshs = GetComponentsInChildren();
foreach (var m in meshs)
{
	m.sortingOrder = characterOrder;
}

シェーダーでは、以下のようにZWrite/ZTestを無効化します。

SubShader
{
	Tags { "RenderType"="Opaque" }
	LOD 100
	ZTest Off
	ZWrite Off
	//略
}

詳細については、github上のコードを見て頂いた方が早いかも知れません。

さて、この手段で最低限の要件は満たされましたが、より表現力を高めるにはどうすると良いでしょうか。

ステンシルバッファ

マスク画像を使ったような処理ができ、隠れたピクセルに対して処理をかける事ができます。この機能を使い、髪の毛に隠れたまゆげの色を操作してみます。
https://docs.unity3d.com/ja/current/Manual/SL-Stencil.html

© UTJ/UCL

  • 髪の毛でステンシルバッファに1を設定
  • まゆげでステンシルバッファに+1する
  • 重なった個所は2になっているので、そこに色が異なるまゆげを描く

という流れを取ります。

ただステンシルバッファを利用する場合、まゆげの描画順制御、まゆげ専用シェーダーだけでは制御しきれません。髪の毛の描画時にステンシルバッファへのアクセスが必要となるため、Unityちゃん付属のシェーダーにも手を加えていく必要があります。
以下のように既存シェーダーを修正、新規シェーダーを追加していきます。

	Pass
	{
		Cull Back
		ZTest LEqual
		//ステンシル用の設定
		Stencil {
			Ref 1
			Comp Always
			Pass Replace
			Fail Keep
			ZFail Keep
		}
		//略
	}
	Pass
	{
		Stencil {
			Ref 1
			Comp Always
			Pass Keep
			Fail Keep
			ZFail IncrSat
		}
		//略
	}

こちらも詳細はgithub上のコードを参照頂くとわかりやすいと思います。

上記以外の手段も存在します。

DstAlpha を用いたアルファブレンド

フレームバッファに書き込まれたアルファ値と演算を行うことで、まゆげに重なる髪の毛の描画を抑制することができます。
https://docs.unity3d.com/ja/current/Manual/SL-Blend.html
こちらの動画、23:35近辺に詳しい説明があります。
https://vimeo.com/57195092

アルファブレンドを用いる場合、まゆげ→髪の毛の順で描画する必要があります。シーン中の構成にも依存しますが、sortingOrderによる描画順の制御を行い手前から奥への順番に描画を行うと、意図した結果が出せます。
具体的には、まゆげにアルファ値を持たせて描画し、その後アルファブレンドを行いつつ髪の毛を描画することで、まゆげをマスクして描画することが可能です。
注意点としては、まゆげはフレームバッファに書き込まれたアルファ値によってマスクされるため、事前に適切にクリアしておく必要があります。例えば、CameraのClear FlagsをSolidColorに設定してアルファ値を1にしておく、などです。

説明は以上となりますが、いかがでしたでしょうか。基本的な機能ですが、描画順の制御により色々な事が実現できることがわかったかと思います。
開発中プロジェクトでのお役に立てば幸いです。