e.blog

主にUnity/UE周りのことについてまとめていきます

SkinnedMeshとBoneWeightについてメモ

概要

とある実装をしていて、アニメーション周りをわりと触ったのでそのメモです。
具体的には、メッシュカットを利用してメッシュを分断したあと、分断したメッシュもボーンアニメーションさせたくて実装したメモです。

実際に切断してアニメーションさせた動画↓

見てもらうと分かりますが、カットされて分断されても同じようにアニメーションしているのが分かるかと思います。

ちなみに、ボーンアニメーション(スキニング)については、前にWebGLで実装した際に解説した記事があるので、興味がある人は読んでみてください。

qiita.com

また、いつもお世話になっているマルペケさんのところの記事を参考にしたので、こちらのほうがもっと詳細です。

その61 完全ホワイトボックスなスキンメッシュアニメーションの解説

原理

スキニング自体の実装や概念については、上記の記事を参考にしてみてください。
ここでは、あくまでメッシュカットをしたときに実装した内容を元に、Unity上でどうしたらいいか、という視点から書いていきます。

とはいえ、簡単なボーンアニメーションの仕組みについて触れておきます。

ボーンアニメーションとは、ボーンと呼ばれる頂点群を定義し、そのボーンの動きに応じてポリゴンの各頂点が動かされる、という仕組みです。
では、どのボーンが動いたらどの頂点が動くのか。それを定義するのがBoneWeightです。

名前の通り、ボーンのウェイト、つまり「影響度」の値を保持します。
なので、メッシュは頂点数と同じだけのBoneWeight配列を保持しています。

BoneWeightは最大で4本までのボーンが設定できます。
ここで勘違いしないでもらいたいのは、モデル全体で4本だけ、ではなく、あくまで「1頂点に影響を与えることができるボーンは最大で4本」ということです。
これはマルペケさんの記事を見てもらうといいと思いますが、とにかくボーンの数を増やしてしまうとGPUで扱えるメモリの領域(レジスタ)がすぐに枯渇してしまうためです。

また、頂点に着目すれば、そもそも4本以上のボーンから影響を受けるケースというのは稀でしょう。
つまり、逆を言えば4本で十分とも言えます。

なのでBoneWeight構造体の定義を見てもらうと分かりますが、boneIndex0boneIndex3weight0weight3まで、配列ではなくそのままの名称で定義が書かれています。

ここで、boneIndexは、各ボーンに振られたインデックス番号、wegithは、そのボーンからどれくらいの影響を受けるかを示したものになっています。
なお、4本のボーンの影響度の合計は必ず1になるように正規化されます。

ボーンの簡単な説明は以上です。

今回はこのボーンの影響度を、メッシュカット後の各メッシュに適用し、それをアニメーションさせる、という実装を行いました。

ちなみに、メッシュカットについては以前、公開されていたコードを読んで解説した記事を書いたので、興味がある方はそちらも合わせてご覧ください。

qiita.com

登場人物

今回の実装にあたって、必要なクラスやプロパティなどがいくつか絡み合っているのでそれらを明確にします。

  • Mesh
  • SkinnedMeshRenderer
  • Animator
  • BoneWeight

Meshクラス

まずはMeshクラス。

Mesh mesh = new Mesh();
mesh.name = "Split Mesh";

// _cuttedMeshesは独自で定義したメッシュ情報を持つクラスの配列
mesh.vertices = _cuttedMeshes[i].Vertices;
mesh.triangles = _cuttedMeshes[i].Triangles;
mesh.normals = _cuttedMeshes[i].Normals;
mesh.uv = _cuttedMeshes[i].UVs;
mesh.subMeshCount = _cuttedMeshes[i].SubMeshCount;
for (int j = 0; j < _cuttedMeshes[i].SubMeshCount; j++)
{
    mesh.SetIndices(_cuttedMeshes[i].GetIndices(j), MeshTopology.Triangles, j);
}

mesh.bindposes = oriRenderer.sharedMesh.bindposes;
mesh.boneWeights = _cuttedMeshes[i].BoneWeights;

Meshはポリゴンを形成するために必要な情報をまとめて持っているクラスです。
verticestrianglesnormalsuvsubMeshCountsubMeshIndicesなどがそれに当たります。

今回はメッシュを平面で切って分割するため、新しく計算した頂点群などを再設定しています。

そしてさらに、メッシュは(前述のように)ボーンの影響度についても知らなければなりません。(アニメーションさせたい場合)
そこで設定しているのがboneWeightbindposeです。

boneWeightは上で書いた通り、各頂点が影響を受けるボーンの情報です。
そしてbindposeは、「モデルのデフォルト位置としてのボーンの姿勢」です。

人型のモデルであれば、Tポーズ時の位置、と考えるといいと思います。
つまり、「動いていない状態のボーンの位置」ということですね。

なぜこれを定義しているかというと、ボーンというのは「動いた差分分だけ、頂点に影響を与える」ようになっているからです。
つまり、ボーンがまったく動かされないならTポーズのまま、ということですね。

そしてそこから、ボーンが曲がったり移動したりすることで、モデルは様々な形に変形されます。
さらにそれを連続して行うことでアニメーションが成立している、というわけです。

SkinnedMeshRenderer

アニメーションするために必要なSkinnedMeshRenderer
ボーンに対応した頂点を、ボーンの動きに追従して動かすことでアニメーションを行います。

そしてSkinnedMeshRendererはその名の通り、スキニングされるメッシュを表示するためのクラスになります。
そのため、いくつかのボーンなどの情報を持たせる必要があります。

// objは新規生成されたGameObject
SkinnedMeshRenderer renderer = obj.GetComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.materials = mats;
renderer.bones = newBones;
renderer.rootBone = newRootBone;

メッシュは頂点群や、それらのボーンの影響度を持つ「データ構造」と言っていいでしょう。
そしてSkinnedMeshRendererは、このデータ構造を用いて適切にボーンの影響を頂点に与えます。まさにレンダラですね。

ボーンアニメーションに必要な情報はここではふたつ。
ひとつはbones、そしてもうひとつがrootBoneです。

rootBoneは、どのボーンがルートになっているのかを示す、Trasnformへの参照です。
そしてbonesは、定義されているボーンへの参照の配列になっています。

ちなみに、なにかしらのモデルデータを開いてみると分かると思いますが、ボーンとして利用されるオブジェクトのツリー構造は、必ずしもボーンだけとは限りません。
例えばなにかをグルーピングするための目的で配置されているオブジェクトなどもあるでしょう。

つまり、ルートボーン以下にはボーンとして機能していないオブジェクトも当然ながら存在します。
そのため、ボーンとしてアサインされたもののみをbones配列が持っている、というわけですね。

Animator

メッシュやレンダラの設定を適切にしても、アニメーション自体を再生しないことには動きません。
ということで、Animatorクラスが必要になります。

今回の実装では、メッシュカット対象オブジェクトが持っていたアニメーションのデータをそのままコピーして使っています。

Animator anim = newObj.AddComponent<Animator>();
anim.runtimeAnimatorController = _target.transform.root.GetComponent<Animator>().runtimeAnimatorController;
anim.avatar = _target.transform.root.GetComponent<Animator>().avatar;

AnimatorAddComponentして、適切にアニメーションに必要なデータを設定(コピー)します。

AnimatorAnimatorControllerについては、Unityでアニメーションさせたことがある人には特に説明の必要はないでしょう。
今回はコピーしていますが、切断後のアニメーションを変える場合は単なるコピーではなく、新しいコントローラを設定してもいいと思います。

そして最後にAvatarをコピーして終了です。

まとめ

今回はあくまでボーンをどうアサインしていったらいいかのメモなので以上となります。
まとめると、

  • メッシュに頂点情報およびボーンウェイト情報とボーンのデフォルト位置を紐付ける
  • SkinnedMeshRendererに、ルートボーンと、アニメーションに使われるボーンの配列を登録する
  • Animatorを通じてアニメーションを行い、AnimatorControllerのステートマシンを用いて各アニメーションを制御する

という流れになります。
データ構造的にはとてもシンプルですね。

次回の記事では、メッシュカットにボーンアニメーションを設定したあたりを書こうと思います。

UNETを使ってネットワーク間で処理を同期する

概要

前に使っていたNetworkViewを使った処理はdeprecatedになり、新しく UNET という仕組みが登場しました。
今回はこれを使っていくにあたって色々新しい概念が出てくるのでそれをまとめておこうと思います。

用語整理

UNETを使う上で、(慣れるまでは)色々混乱する用語があるので、それをまずは整理しておきます。

サーバ

UNETはサーバとクライアントのコードを同じクラス上で記述することができます。
そのため、サーバ側のコードを別途用意する必要はありません。
ただ、実行されるのがサーバ側なのかクライアント側なのか、というのは常に意識しておかないとなりません。

リモートクライアント

サーバに対してのクライアントです。
つまりサーバに接続しているPCということですね。

ローカルクライアント

こちらもリモートクライアントと「ほぼ」同じものです。
サーバに接続し、リモートクライアントと同じように、クライアント側のコードが実行されます。
ただ一点違うのは、以下のホストに関係しています。

ホスト

UNETはサーバ/クライアント側のアーキテクチャなので、必ずサーバが必要となります。
ただ、サーバをどこかにホストして実行する以外の手段として、いずれかのPCがホストとなり、サーバとクライアントふたつを同時に起動する形のものがあります。それを「ホスト」と呼んでいます。

つまり、前述の「ローカルクライアント」は、サーバが実行されているPC上で実行されているクライアント、ということになります。
そして次の名前が似ている「ローカルプレイヤー」と最初は(自分は)とても混乱しました。

ローカルプレイヤー

ローカルクライアントとローカルプレイヤー。どちらもローカルとついていて、なんとなく似たようなイメージの両者。
ただ、ローカルクライアントはネットワーク周りのもになのに対して、ローカルプレイヤーはどちらかというとゲームよりの概念となっています。

誤解をおそれずに言うなら、ローカルプレイヤーとは、そのPC上でプレイヤーを操作しているユーザのこと、と考えてもいいと思います。
当然、マルチプレイゲームであればユーザはプレイしている人の数だけいることになりますが、実際に操作している人のPC上に映し出される、操作可能なキャラクターは大体の場合においてひとりでしょう。
そしてその「操作可能なキャラクター」こそがローカルプレイヤーと思っていていいと思います。

(実際はマルチプレイをする上で、権限だったり、といったことを示すものですが、あくまでイメージはそんな感じです)

ネットワークシステムの概要

詳細についてはドキュメントを見たほうが確かなのですが、ざっくりと自分の認識とポイントを書いておきたいと思います。
以下はドキュメントにある画像です。

https://docs.unity3d.com/jp/current/uploads/Main/NetworkHost.png

ポイントは、用語整理のところでも書いた通り「Host」と書かれた領域に「Local Client」と「Server」があることでしょう。
ドキュメントを読み進めていくにあたって地味に重要なので、違いはしっかり認識しておいたほうがいいと思います。

まずは同期を試してみる

さて、UNET、最近では色々と実装してだいぶ慣れてきましたが、最初の頃はなにをどうしたらいいんだ、という感じでした。
簡単に同期させるだけでもいくつかのクラスやコンポーネントを組み合わせないとならないからです。

なので、まずは簡単に「同期させるだけ」をベースに説明していこうと思います。

同期する上で中心となる「NetworkIdentity」コンポーネント

まず、ネットワーク間で同期を取るオブジェクトには必ずこの「NetworkIdentity」コンポーネントをつける必要があります。
ネットワークで同期を取るということは、それぞれ同期対象のオブジェクト(やキャラクター)を一意に識別し、それを適切に取り扱う必要があります。

そのためのIdentityを司るのが、まさにこの「NetworkIdentity」というわけです。
色々な同期用コンポーネントをつけると必ずこのコンポーネントが一緒にくっついてきます。

位置を同期する「NetworkTransform」コンポーネント

次に、位置を同期したい場合は「NetworkTransform」コンポーネントをアタッチします。
(位置以外にもアニメーションの同期などもあります)
このコンポーネントをアタッチするだけで簡単に位置の同期を行ってくれます。
位置の同期だけであればこれで十分です。

f:id:edo_m18:20170120164537p:plain

ネットワーク間で「オブジェクトの生成」を同期する

さて、位置の同期だけであれば前述のようにコンポーネントを付与するだけでOKでした。
しかし、オブジェクトを新しく生成する場合はそこまで簡単にはいきません。

必要な処理を列挙すると以下のようになります。

  • 生成したいオブジェクトをPrefab化する
  • 該当オブジェクトに「NetworkIdentity」コンポーネントを付与する
  • NetworkManagerに、ネットワーク間で生成を同期したいオブジェクトを登録する(ClientSceneのRegisterPrefabメソッドからも登録できる)
  • 生成したオブジェクトをNetworkServer.Spawnメソッドを利用して、接続している全クライアントに対してオブジェクトが生成されこと(Spawnされたこと)を通知する。

という流れです。
よくよく考えてみれば当たり前ですが、オブジェクトが生成されたことを、接続中のクライアントに通知しないと画面にそれが表示されません。
そのために、Spawnシステムがあるんですね。

コードで書くと以下のようになります。(実行はサーバ上で行う必要があります)

GameObject obj = Instantiate(_anyPrefab);

// これを実行することで、
// サーバで生成されたオブジェクトを全クライアントに通知することができる。
NetworkServer.Spawn(obj);

ネットワークで同期する処理を記述する(NetworkBehaviourを継承する)

オブジェクトの位置を同期したり、生成したり、といったことは大体はコンポーネントの付与で対処できました。
しかし、それだけでは当然ゲームは作れません。
ゲーム足り得るには、やはりユーザの操作を受け付けてなにかしらのフィードバックを行う必要があるでしょう。

そしてそれは当然、プログラムを書く必要があります。

通常、Unityは「MonoBehaviour」クラスを継承することで様々なことを記述していきます。
UNETもこれとコンセプトは変わりません。
ただ、代わりにNetworkBehaviourクラスを継承して処理を記述する必要があります。

NetworkBehaviourクラスはisServerisLocalPlayerなどのプロパティを持っており、今実行しているコードがサーバなのかクライアントなのか、ローカルプレイヤーなのかなどの状況を判別するためのフラグが用意されています。 (ひとつのコードで、サーバ側、クライアント側、あるいはその両方として実行させることができる)

サーバ or クライアント、どちらで実行するか

冒頭では、サーバとクライアントどちらも同一に書けると書きました。
基本的にはオフラインのゲームでの制作と考え方はあまり変わりません。

しかし、中にはサーバ側だけで実行してほしい処理、あるいは逆にクライアントだけで十分な処理などがあると思います。
そうした、「どちら側で実行するのか」を決める方法があります。

ServerServerCallbackClientClientCallback、この4つのAttributeを利用することでそれを実現します。
それぞれの意味は以下のようになります。

  • Server ... サーバ側でのみ実行される処理。クライアント側では空実装になる。
  • Client ... サーバの逆、クライアント側のみで実行される処理になる。
  • ServerCallback ... Serverと似ていますが、UpdateメソッドのようにUnityのシステムから呼ばれるメソッドに対して記述します。
  • ClientCallback ... こちらもServerCallbackの逆ですね。

例えば、サーバ側だけで処理してほしいメソッドの場合は、[Server] Attributeを付与します。

[Server]
void AnyMethod()
{
    // do anything.
}

「ローカルプレイヤー」であるかどうか

位置の同期、サーバ/クライアント双方での処理の記述について書きました。そして、実際に制作するにあたってとても重要なのが処理している対象が「ローカルプレイヤーであるか」という点です。
ローカルプレイヤーはその名の通り、コードを実行している対象が「プレイヤー」であるかどうかを考えることが大事なポイントとなります。
コードで言うと以下のようなコードで生成されたGameObjectインスタンス)のことです。(サーバ側のコードとして実行してやる必要があります)

var player = Instantiate(_playerPrefab);
NetworkServer.AddPlayerForConnection(conn, player, playerControllerId);

ためしにプレイヤーキャラと敵キャラの2体だけが存在しているところをイメージしてください。

明らかに「敵キャラ」は「プレイヤー」ではありませんね。
敵キャラ(GameObject)にはきっとEnemy.csのようなスクリプトがアタッチされていると思います。

この場合のEnemy.csコード内で実行している処理は「プレイヤー」ではないコードとなります。

敵キャラなどは以下のようなコードで生成、同期されます。

GameObject obj = Instantiate(anyPrefab);
NetworkServer.Spawn(obj);

一方、プレイヤーキャラの場合は前述のような(AddPlayerForConnectionメソッド)コードで生成されたインスタンスです。
そしてそのGameObjectにはPlayer.csのようなスクリプトがアタッチされていることでしょう。
このコード内で実行されている処理は当然、「プレイヤー」として実行されます。

さて、オフラインゲームであれば「プレイヤーキャラ」はひとつだけですが、オンラインゲームとなるとこの「プレイヤーキャラ」は接続しているユーザの数だけ存在することになります。
大体のオンラインゲームは、自分の分身となるアバター的なキャラクターを操作してゲームを進めていくと思います。

では、その「アバター的キャラクター」と、「他人が操作しているキャラクター」を分けないとどうなるでしょうか。
自分の操作した内容がすべてのキャラクターに伝わってしまったらゲームどころではありませんね。

つまり、「自分のキャラクター」を識別した上で、コントロール可能なキャラクターをひとつに絞らないとなりません。(例えすべてのキャラクターの見栄えが同じであっても)

そしてこの「自分のキャラクター」を表すのが「ローカルプレイヤー」という概念なのです。

なので、ローカルプレイヤーとして動いているゲームオブジェクトの操作は「権限」がある状態になっていて、UNETでは他キャラクターと異なる意味を持ちます。

権限 - Local Client Authorityについて

NetworkIdentityにはLocal Client Authorityというフラグがあります。 これは、ローカルクライアントが権限を持つ可能性があるオブジェクトに対して設定します。

具体例をあげると、プレイヤーが、ゲームワールドに落ちているオブジェクトを拾ってなにかアクションをする、というような場合に利用します。
通常、こうしたオブジェクトの挙動については、該当オブジェクトにスクリプトファイルをアタッチして制御すると思います。
仮にオフラインのゲームであれば「拾った」ことを検知して、プレイヤーの手にそれを持たせるなどの処理をかけば目的が達成できます。
(もちろん、ゲーム的に意味を付けるならもっと色んなことを書かないとダメですが)

しかし、オンラインゲームの場合、その「拾ったオブジェクト」はサーバおよび全クライアントで表示されています。
そしてその位置を動かした場合。誰の位置を信頼したらいいでしょうか。
サーバ側? でも、位置のアップデートはたった今拾ったプレイヤーの位置や行動によって変更されます。

すべての処理をサーバ側だけで行っているのであればサーバ側だけで判断して、該当プレイヤーの状態に応じて位置を更新してやれば動きます。
しかし、全プレイヤーの描画や姿勢制御などをサーバだけで行うには限界があります。
そのあたりはクライアントでやってほしいところですよね。

ここで、見出しになっている「権限」の話が出てきます。
つまり、該当のオブジェクトを「操作」する「権限」を「誰が持っているのか」を表すのが、LocalClientAuthority、というわけです。

そしてこのフラグがtrueになっているローカルプレイヤーの操作を信頼し、サーバはその状態を各クライアントに伝える、という形で位置同期が行われています。

また、下のほうの「サーバ・クライアント間でメッセージを送信する」でも触れますが、UNETでは「コマンド」という形でサーバに対してメッセージ(メソッド呼び出し)を行うことができます。
ただこの「コマンド」、今説明したLocalClientAuthorityがないプレイヤーが実行しても無視されてしまいます。

位置の同期だけでなく、そうした様々な実行権限を表しているわけですね。
コードにするとこんな感じ↓

[Command]
void CmdAssignAuthority(NetworkIdentity targetID, NetworkIdentity playerID)
{
    targetID.AssignClientAuthority(playerID.connectionToClient);
}

[Command]
void CmdRemoveAuthority(NetworkIdentity targetID, NetworkIdentity playerID)
{
    targetID.RemoveClientAuthority(playerID.connectionToClient);
}

基本的な概念はこのような流れで制御されていきます。
あとは、サーバ・クライアント間でのメッセージのやり取りや、プレイヤーの生成など実際のコードを書いていくフェーズになります。

以下は、実際に開発をする上でハマった点や役に立った点などをざっくばらんにまとめています。

その他、開発中に必要になったことのメモ

NetworkInstanceIdを使ってオブジェクトを特定する

基本的に、サーバ/クライアント間でオブジェクトを特定して処理をさせる場合、GameObjectの参照をそのまま渡すことはできません。
(よくよく考えれば当たり前ですが、サーバ/クライアント間ではメモリも共有されていませんし、参照を渡したところでなんの意味もありません)

そこで利用するのがNetworkInstanceIdです。

NetworkInstanceId は、サーバ/クライアント間で同期する際にオブジェクトを識別するためのIDを表すクラスです。 これを利用して、同期対象となるオブジェクトを特定し、オブジェクトに対して色々と処理を行うことができます。

サーバとクライアントで利用するクラスが異なる

この NetworkInstanceId ですが、サーバ側とクライアント側で利用する方法が異なります。
サーバ側で処理を行う場合は以下のようにします。

// サーバ側
GameObject target = NetworkServer.FindLocalObject(netId);
クライアント側で処理を行う場合は以下のようにします。

// クライアント側
GameObject target = ClientScene.FindLocalObject(netId);

あとは、取得したGameObjectに対して通常時と同じように処理をしてやればOKです。

ただひとつ注意点として、ここで行った処理はあくまで「そのクライアント上でのみ」実行されるという点です。
つまり、処理は同期されません。同期させたい場合はRPCを使って処理する必要があります。

変数の同期

UNETにはフィールドの値を同期する仕組みが用意されています。
同期したいフィールドにSyncVarアトリビュートをつけることで、サーバを経由して値を同期することができます。
ただしいくつか制限があり、通常のプリミティブ型か Transform や Quaternion などの特定の型のみしか同期することができません。 当然、自分で定義したクラスなどは同期できないので注意が必要です。

プレイヤーオブジェクトの取得

シーンに配置されたオブジェクトのうち、プレイヤー(UNET上では若干特殊なものとして扱われる、いわゆる操作可能なプレイヤーオブジェクトのこと)を取得する場合は以下のようにすることで取得できます。

foreach(var player in ClientScene.localPlayers)
{
    // クライアントシーンに存在するローカルプレイヤーのうち、操作に必要なものを取り出して処理
}

ここで、 ClientScene はサーバと区別されている、いわゆるクライアント上のシーンに属するプレイヤーオブジェクトを取り出すことができます。 (localPlayers になっているのは、複数生成することが可能?)

その他の、サーバで管理されているオブジェクトの取得

上記はプレイヤーオブジェクトの取得でした。 今度は、プレイヤーオブジェクト以外の、サーバで管理されているオブジェクトの取得です。(Spawnしたやつ)

foreach(var dict in ClientScene.objects)
{
    // サーバで管理されているオブジェクトの辞書。
    // Dictionary<NetworkInstanceId, NetworkIdentity>型のオブジェクトで、サーバで生成された、プレイヤー以外のオブジェクトが格納されている
}

NetworkBehaviourで利用できるコールバック

  • OnStartServer
  • OnStartClient
  • OnSerialize
  • OnDeSerialize
  • OnNetworkDestroy
  • OnStartLocalPlayer
  • OnRebuildObservers
  • OnSetLocalVisibility
  • OnCheckObserver

手動でプレイヤーオブジェクトを生成、登録する

色々調べてもなかなか情報が出てこず、だいぶ苦戦しました・・。 が、実に簡単なことでした。 (なんかコネクション張ってそのあとになにしてかにして、みたいのを想像していたので・・)

void Update()
{
    if (!IsClientConnected())
    {
        return;
    }
    
    // サーバに追加を通知し、成功したら `true` が返る
    if (ClientScene.AddPlayer(0))
    {
        Debug.Log("Success to add a player.");
        
        // ローカルプレイヤーの情報にアクセス。
        // ただし、サーバとの通信が挟まるため、非同期で`gameObject`が設定されるため、
        // すぐにアクセスするとNullになるので注意。
        ClientScene.localPlayers[0].gameObject.transform.position = new Vector3(0, 10f, 0);
    }
}

プレイヤーの生成(スポーン)を出し分ける

オンラインゲームは、ほとんどの場合、プレイヤーの見た目や挙動は、すべてのプレイヤーで同一、というケースは稀でしょう。 むしろ、すべてのプレイヤーは異なっているのが普通だと思います。

UNETは色々なものを抽象化してくれていますが、それが故に、普通にチュートリアルを読んでセットアップしただけでは、決まったプレイヤーのPrefabしかスポーンさせることはできません。

そこで、以下のようにすることで、任意のクラス(やPrefab)を用いてプレイヤーを生成することが可能となります。

// Prefabを複数用意しておく
[Header("プレイヤー1のPrefab"), SerializeField]
GameObject _player1Prefab;

[Header("プレイヤー2のPrefab"), SerializeField]
GameObject _player2Prefab;

// ...中略...

// 1 = player1, 2 = player2
var message = new IntegerMessage(isPlayer1 ? 1 : 2);
if (!ClientScene.AddPlayer(ClientScene.readyConnection, 0, message))
{
    Debug.Log("Failed to add a player.");
    return;
}

// .. 中略...

// クライアント側から「プレイヤー」の追加要求があった場合に実行される
public override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader reader)
{
    Debug.Log("[NETWORK] OnServerAddPlayer with message.");

    var message = reader.ReadMessage<IntegerMessage>();
    var player = message.value == 1 ? Instantiate<GameObject>(_player1Prefab) : Instantiate<GameObject>(_player2Prefab);
    NetworkServer.AddPlayerForConnection(conn, player, playerControllerId);
}

重要なポイントは2点です。

ひとつめは、クライアント側でpublic static bool AddPlayer(Networking.NetworkConnection readyConn, short playerControllerId, Networking.MessageBase extraMessage);を利用してサーバに要求を送ります。

このメソッドは、現在張られているコネクションを用いて、コントロールIDおよび付加情報を持ってサーバにリクエストを送ります。
このとき、extraMessageに、どのプレイヤータイプを生成すればいいかを格納してリクエストを送っているわけです。
ちなみに、上記サンプルではプレイヤーの種類が2タイプなので 1 か 2 をメッセージとして送っている簡単な例です。
もちろん、 Integer 以外にもメッセージとして利用できますし、独自のクラスを定義してそれを利用することも可能になっています。

そしてふたつめ。
ふたつめはサーバ側のハンドリングです。
クライアントからメッセージ付きでリクエストが来た場合はpublic override void OnServerAddPlayer(NetworkConnection conn, short playerControllerId, NetworkReader reader)が呼ばれます。

OnServerAddPlayerメソッドはオーバーロードがあるので注意が必要です。
メッセージ付きの場合は上記のメソッドをオーバーライドして実装する必要があります。

※ 余談ですが、これらのメソッドは「オーバーライド」しないとダメっていうのに最初ハマりました。StartやUpdateはオーバーライドじゃないのに、仕組みが微妙に異なると混乱する( ;´Д`)

さて、これでメッセージに応じて適切にプレイヤーを生成することができました。 今回の例はとても簡単なパラメータのみですが、メッセージをもっと拡張することで、見た目だけではなく、生成時のパラメータの初期化などにも利用することができると思います。

サーバ・クライアント間でメッセージを送信する

ネットワーク対応したコンテンツであれば当然、クライアント・サーバ間でなにかしらのメッセージのやり取りを行いたいものです。 前述の「Command」アトリビュートは主に、各プレイヤーごとに定義された、まさに「コマンド」を実行するものです。

例えて言えば、マルチプレイで相互に接続された各プレイヤーごとの「戦う」などの(RPGと似た意味での)コマンドを実行するもの、と考えると分かりやすいかもしれません。 なので各プレイヤー(ローカルプレイヤー)の振る舞いを実行するためのものが「コマンド」です。

しかし、例えばプレイヤーがなにかしらの行動を行い、例えば「穴に落ちた」など、「コマンド」というよりは「結果」や「アクション」に近いものは、前述のコマンドでは実行できません。 そうした場合に使えるのが、ネットワーク間で通信が行える「Unity - Manual: Network Messages」です。

メッセージを受け取るハンドラを設定する

クライアント・サーバ間でメッセージをやり取りするには、そのメッセージの受信を検知できるようハンドラを登録する必要があります。 (イメージ的には普通のイベントの登録と似たものです)

通常、C#event を用いる場合はデリゲートの宣言とそれにメソッドを追加する形で設定していくのが普通かと思います。 しかし、UNETのクライアント・サーバ間でのやり取りはそれとは異なり、メッセージタイプを表すID(short型の値)を指定してメッセージを送信する方法を取ります。 (ちょうど、ポート番号を指定してコネクションを張ってやり取りするようなイメージでしょうか)

ちなみにメッセージタイプは予約されている番号がいくつかあり、独自のメッセージをやり取りする場合はそれを避けて定義する必要があります。

docs.unity3d.com

基本はピンポン?

なにがしかのイベントがあり、それをすべてのクライアントに伝えたい場合は多いでしょう。 その場合、クライアント側だけで処理をしてしまっても、それはローカルの状態が変わっただけでオンライン上の他のユーザへは反映されません。 現在接続しているユーザすべてに状態を反映させるためには、サーバ側から全クライアントに対してRPCを利用して命令を飛ばしてやる必要があります。

そのため、とあるクライアントで起きたイベントをいったんサーバに通知し、それを持って全クライアントに改めて通知してもらう、という方法を取る必要があります。

    void InvokeEvent()
    {
        if (hasAuthority)
        {
            CmdInvokeEvent();
        }
        else
        {
            GameManager.Instance.GetAuthority(gameObject, () =>
            {
                Debug.Log("in callback.");
                CmdInvokeEvent();
            });
        }
    }

    [ClientRpc]
    void RpcInvokeEvent()
    {
        _event.Invoke();
    }

    [Command]
    void CmdInvokeEvent()
    {
        //_event.Invoke();

        RpcInvokeEvent();
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Player"))
        {
            if (GameManager.Instance.IsLocalPlayer(other.gameObject))
            {
                InvokeEvent();
            }
        }
    }

手動でNetworkManagerHUDと同様の処理を行う

NetworkManagerHUDを利用すると、マネージャクラスを使って簡単にサーバ起動、クライアントのコネクションを行ってくれます。 が、細かい処理を行おうとするとこのあたりも自分で実装するケースが出てきそうです。 その場合には自分でNetworkManagerHUD相当のことをしないとなりません。 それについてはこちらの記事にコード例を載せてくれていたので紹介します。

なお、簡単にホストの起動とクライアント接続をするのみならば、以下のようにすることで対応可能です。

        void OnGUI()
        {
            if (!IsClientConnected())
            {
                RenderSelectUNET();
                return;
            }

            if (NetworkClient.active && !ClientScene.ready)
            {
                ClientScene.Ready(client.connection);
                return;
            }


            if (_isSelected)
            {
                return;
            }

            RenderSelectPlayerType();
        }

        private bool _isConnected = false;
        private NetworkClient _client;

        void RenderSelectUNET()
        {
            int btnWidth = 80;
            int btnHeight = 20;
            int left = 10;
            int top = 10;

            if (GUI.Button(new Rect(left, top, btnWidth, btnHeight), "LAN Host"))
            {
                StartHost();
                client.RegisterHandler(MsgType.Connect, OnConnected);
                //string address = networkAddress == "localhost" ? "127.0.0.1" : networkAddress;
                //NetworkServer.Listen(address, networkPort);

                //_client = new NetworkClient();
                //_client.RegisterHandler(MsgType.Connect, OnConnected);
                //_client.Connect(address, networkPort);
            }
        }

大事な点は、NetworkClientで接続を行い、ClientScene.Readyを読んで、クライアントサイドの準備が完了したことを通知する点です。

その他メモ

同期を滑らかにする

Movement Thresholdを小さくすることで、小さな動きでも同期をかけてくれるため滑らかになります。(まだ検証していませんが、その分同期処理が多く走ることになるので負荷はあがると思います)

Interpolate Movement Factorを小さくすることで補完をかける閾値を変えることができます。つまりそれだけ多く補完をかけることになるのでより滑らかになります。

f:id:edo_m18:20170203111446p:plain

uGUIのEventSystemを理解する

概要

Unity4.6から導入されたEventSystem。 調べてみると色々と学びがあるのでメモとして残しておきます。 (若干、今さら感がありますが)

まず最初に大事な概念として、このシステムはいくつかのクラスが密接に連携しながらイベントを処理していく形になっています。アーキテクチャ的にも学びがある実装になっています。

登場人物

このシステムを構成する、必要な登場人物は以下にあげるクラスたちです。

  • BaseInputModule
  • BaseRaycaster
  • ExecuteEvents
  • PointerEventData
  • RaycastResult

すごくおおざっぱに言うと。

BaseInputModuleが全体を把握し、適切にイベントを起こす、まさに「インプット」を管理するクラスとなります。

まず、BaseRaycasterによって対象オブジェクトを収集します。(複数のRaycasterクラスがある場合はすべて実行して、対象となるオブジェクトをかき集めるイメージ) そして得られたオブジェクトに対してどういう状態なのかを判断します。例えば、ホバーされているのか、ホバーが解除されたのか、あるいはクリックされたのか、などなど。

そしてその判断された状態に応じて、ExecuteEventsを利用してイベントを送出する、というのが全体的な流れになります。

カスタムする

Baseと名前がついていることから分かる通り、これらを適切に継承・使用することで、独自の仕組みで対象オブジェクトを決定し、独自のイベントを伝播させる、ということも可能になります。

BaseInputModuleを継承したカスタムクラス

BaseInputModule を継承したカスタムクラスを作成することで、独自のイベントを定義することができます。そもそも冒頭で書いたように、このクラス内でオブジェクトの収集を行い、イベントの状態管理をするのが目的なのでそれを行うためのクラスとなります。

なお、 BaseInputModuleUIBehaviour -> MonoBehaviour を継承しているクラスのため、GameObjectにアタッチすることができるようになっています。

※ ちなみに、シーン内でアクティブなInputModuleはひとつだけと想定されています。

Processメソッドのオーバーライド必須

BaseInputModule には Process メソッドがabstractで定義されており、これのオーバーライドは必須となっています。この Process メソッドは、BaseInputModule 内で自動的に呼ばれ、Updateと似たような形で毎フレームごとに呼ばれるメソッドとなっています。

なので、作成したカスタムクラスを適当なGameObjectにアタッチし、Process メソッド内に処理を書くと毎フレーム呼ばれるのが確認できると思います。

BaseRaycasterを継承したカスタムクラス

BaseRaycasterを継承したカスタムクラスを作成することで、収集の対象とするオブジェクトを独自で定義することができます。このベースクラスのRaycastメソッドとeventCameraプロパティはabstractで宣言されており、派生クラスではoverride必須となっています。

Raycastメソッドのオーバーライド

BaseRaycasterRaycastメソッドをオーバーライドすることで、EventSystem.RaycastAllメソッド実行時に収集対象が収集されます。

RaycastAllメソッドのシグネチャは以下のようになっています。

public void RaycastAll(PointerEventData eventData, List<RaycastResult> raycastResults);

引数として、ポインターイベントのデータオブジェクトと、Raycast結果を格納するList<RaycastResult>型のオブジェクトを渡します。このRaycastAllメソッド内で、BaseRaycasterを継承したクラスのRaycastが順次呼び出されるので、その処理の中でヒットしたオブジェクトをリストに追加します。

最後に、全RaycasterのRaycast結果を元にして、対象オブジェクトにイベントを送出します。

Raycastメソッドで対象オブジェクトを収集する

Raycastと名がついていますが、別にRaycastを必ず実行しないとならないわけではありません。
あくまで、Raycastが判断として使われているためについている名前でしょう。つまり、このメソッド内で適当なオブジェクトを結果リストに入れてあげれば、Raycastをしていなくともそれが「ヒット候補」として送られることになります。

RaycastResult

RaycastResultオブジェクトは、Raycastした結果を保持するオブジェクトです。Raycasterによって収集されたオブジェクトの結果を保持するもの、と考えるといいと思います。

BaseRaycasterクラスを継承したサブクラスのRaycast実行時に、結果を保持する際に使用します。

ライフサイクル

全体の簡単なライフサイクルを見てみましょう。

  • BaseInputModule.Process
  • EventSystem.RaycastAll
  • 各BaseRaycasterを継承したクラスのRaycast
  • ExecuteEventsを利用してイベントを伝達

大まかに言えば、上のようなライフサイクルでイベントを実行していきます。以下に、動作原理を理解するためだけの、とてもシンプルなコード例を載せておきます。

InputModuleサンプル

最初はInputModuleのサンプルです。
Processメソッド内で対象となるオブジェクトを集めます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputModule : BaseInputModule
{
    private List<RaycastResult> _result = new List<RaycastResult>();
    private PointerEventData _eventData;

    private void Awake()
    {
        _eventData = new PointerEventData(eventSystem);
    }

    public override void Process()
    {
        _result.Clear();
        eventSystem.RaycastAll(_eventData, _result);
        Debug.Log(_result[0].gameObject);
    }
}

PointerEventDataは、UIを操作するための「ポインタ」の位置や状態などを保持するデータです。
このデータを元に、Raycasterは対象オブジェクトがどれかを識別します。

なので、実際にはRaycastAllメソッドに渡す前に、適切に「現在の」ポインタの状態にアップデートする必要があります。が、今回はサンプルなので生成するだけに留めています。

Raycasterサンプル

次に、InputModule内で実行されるRaycastです。実際にはeventSystem.RaycastAllを通じて間接的に実行されます。

このメソッドの第二引数に渡しているList<RaycastResult>型のリストが、結果を保持するために使用されていることに注目してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class Raycaster : BaseRaycaster
{
    [SerializeField]
    private GameObject _obj1;

    [SerializeField]
    private GameObject _obj2;

    public override Camera eventCamera
    {
        get
        {
            return Camera.main;
        }
    }

    public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
    {
        RaycastResult result1 = new RaycastResult()
        {
            gameObject = _obj1,
            module = this,
            worldPosition = _obj1.transform.position,
        };

        RaycastResult result2 = new RaycastResult()
        {
            gameObject = _obj2,
            module = this,
            worldPosition = _obj2.transform.position,
        };

        eventData.pointerCurrentRaycast = result2;

        resultAppendList.Add(result1);
        resultAppendList.Add(result2);
    }
}

Raycastメソッドには、ポインタの状態としてPointerEventDataが、また結果を保持するためのリストとしてList<RaycastResult>型のリストが渡ってきます。

この第二引数のリストが、上のInputModule内で渡したリストになります。これでなんとなく関係が見えてきたのではないでしょうか。

あとは、レイキャストを実行するなりして「対象オブジェクト」を識別します。今回は動作原理をわかりやすくするため、レイキャストなどは行わず、予め登録されたオブジェクトをそのままリストに追加するだけの処理にしています。

実際にはここで、レイキャストや、その他必要なチェックを経て、実際のオブジェクトを選出することになります。

まとめ

InputModuleが全体のインプットを管理し、イベントシステムに対して適切にRaycastを実行させ、収集したオブジェクト(RaycastResult)を利用して各イベントの伝搬を行う過程がなんとなくわかったかと思います。

InputModuleは、RaycastResultのデータから各オブジェクトの状態(距離など)を使って、そのオブジェクトがホバー状態なのか、それが解除されたのか、などなどを判断し、またそう判断されたオブジェクトに対してはExecuteEventsを使ってイベントを伝達します。

オブジェクトを集める、判断する、イベントを伝える、というのが、いくつかのクラスが密接に連携しながら、かつ拡張性高く実装されているのが分かってもらえたかと思います。

VR向けにuGUIを拡張したりしているので、機会があったらそれも書きたいと思います。

ComputeShaderを触ってみる その1 ~スレッド編~

概要

並列化可能な、膨大な数の計算を行う場合はCompute Shaderの出番です。
今回はこの「Compute Shader」を触ってみたのでそのメモです。

Compute Shaderの最小単位

Compute Shaderを利用する場合、まずは.computeファイルを作成します。
そして作成したCompute Shaderに以下を記述すると最小の構成となります。

#pragma kernel CSMain

[numthreads(4, 1, 1)]
void CSMain()
{
    // do nothing.
}

このCompute Shaderはなにもしてくれませんが、構成がどうなっているかを知るには十分です。
まず、#pragma kernel CSMainで「CSMain」関数がカーネル*1であることを伝えます。
当然ですが、シェーダ内には任意の関数を定義することができます。
その他の言語の関数と何ら変わらず、関数呼び出しを通して計算を行うことができます。

その中で、カーネルとして動作する関数を#pragma kernelで指定している、というわけです。

スレッド

さて、Compute Shaderは(当然ですが)GPU上で動作します。
そしてGPUと言えば並列処理が得意ですよね。
そのGPUに並列処理をしてCPUの代わりに計算を行ってもらうのがCompute Shaderです。

そしてその並列処理を最大限活用するのが「スレッド」です。

スレッド数は[numthreads(x,y,z)]で指定します。
ここで指定した数だけスレッドが作られ、それぞれがカーネルのコピーとして動作します。

冒頭の例では[numthreads(4,1,1)]となっているので全部で4スレッドとなります。

スレッドと次元

なぜ(4,1,1)なのに4スレッドなのか、と思われたかもしれません。

これは、多次元配列としてスレッド数を指定しているから、というのが理由になります。
つまり、X軸に4つ、Y軸に1つ、そしてZ軸に1つの配列をイメージしてください。
Y,Zともに1なので、実際はX軸のみ、1次元の配列になりますよね。

なので合計で4スレッドになる、というわけです。

試しに[numthreads(4,4,1)]と指定すると、X,Y軸が4つずつ、Z軸が1つの、4 * 4 = 16スレッドが生成されることになります。

なぜこんなまどろっこしい方法でスレッド数を指定するのか、というと、GPUが得意としているテクスチャへのアクセスなどが行いやすいから、というのが自分の理解です。
どういうことかというと、テクスチャは2次元で表されます。幅と高さですね。

そしてテクセルをそれぞれ二次元配列として表した場合、左上を(0, 0)、その右隣を(1, 0)などと表現することができます。

こうした、二次元配列へのアクセスには、ふたつの添字があると便利ですよね。
そして複数次元でスレッドを表すことで、このあと説明する「スレッドグループ」とあいまって、とても簡単に、各テクセルへアクセスすることが可能となります。

要は、二次元(ないし三次元)の要素を扱うのだから、それに応じたスレッドの定義をしておいたほうがいいよね、くらいの理解でいいと思います。(というか自分はそんな程度の理解ですw)

スレッドグループ

さて、スレッドの数については前述の通りです。
これに加えて、さらに多次元の「スレッドグループ」というものがあります。

最初はなんのこっちゃな感じですが、まずはC#側のコードを見てみましょう。

// Get kernel ID by string. Just example below.
int kernel = shader.FindKernel("CSMain");
shader.Dispatch(kernel, 1, 1, 1);

コンピュートシェーダを起動するC#コードは上記のような感じになります。
shaderには、インスペクタなどから.computeファイルを指定するなどして参照を保持しておきます。

そしてFindKernelメソッドを利用して、カーネルIDを取得します。
実はカーネルは、ひとつのシェーダ内に複数記述することができ、上から出現順にIDが連番で振られていきます。
(なので、FindKernelメソッドを使わず、直接0などを指定しても動作します。サンプルのように、カーネルがひとつしかない場合は直接0を指定したほうが分かりやすいサンプルになるかもしれません)

通常は複数カーネルが存在し、かつ順番も(リファクタリングなどで)変化するかもしれないため、FindKernelメソッドで適切にIDを取得する形を取ります。

カーネルIDが取得できたら、バッファ(ComputeBuffer)や計算に使う値などを適切にセットアップした上で、Dispatchメソッドを使ってコンピュートシェーダを起動します。

Dispatchメソッドの引数は全部で4つ。

第一引数は、起動するカーネルIDを指定します。
残りの3つは、前述した「スレッドグループ数」を指定するものになります。

上の例ではX,Y,Zそれぞれに1を指定しています。
こちらもスレッド数と同じ考え方なので、結果としてグループはひとつだけ指定したことになります。
そして1グループ、4スレッド、つまり合計4スレッドが一度のDispatchメソッドで実行される、というわけです。

Compute Shaderを利用した計算を行う場合、このスレッドとスレッドグループの概念は必須で覚えないとなりません。
というのも、計算が並列で行われるため、どのスレッドが今実行中なのか、その位置を把握しながらコードを書かないとならないためです。

特に、バッファなどは一次元配列の形でデータを格納するため、「どの場所に計算結果を格納するか」はスレッド番号などから判断しないとなりません。

とはいえ、最初はどういうことかすぐに理解するのはむずかしいと思います。
MSDNにスレッドグループを説明する画像があったので引用します。

f:id:edo_m18:20170502103117p:plain

スレッドIDを取り出す

上の図を見て「あーなるほどね」となったらここは読み飛ばしてもらってOKです。
最初見たときは、さっと眺めただけではよく分かりませんでした。

が、落ち着いて考えればそこまでむずかしい話ではありません。

まず、図で説明されているのは[numthreads(10,8,3)]で合計240スレッドが指定されたコンピュートシェーダに対して、Dispatch(5,3,2)で合計30グループのスレッドグループからなるスレッド群が起動するところを説明しています。(つまり合計7,200スレッド)

図の最初のテーブルは「スレッドグループ」を表しています。
(5,3,2)なので、X軸に5、Y軸に3、Z軸に2の三次元のテーブルとして表現されています。

そして次のテーブルは、(2,1,0)グループのスレッド詳細をクローズアップしています。
240スレッドが全グループそれぞれで実行されているので、ひとつのグループの詳細を見てみると、またさらに三次元のスレッド群で構成されている、という入れ子構造になっているわけですね。

下のテーブルでは今度は(7,5,0)の位置にあるスレッドについて述べています。

実は、コンピュートシェーダのカーネルへの引数は、通常のシェーダと同様、セマンティクスを用いて必要なデータをシステムから渡してもらうことができます。
そのセマンティクスが、図の下に書かれているSV_GroupThreadIDSV_GroupIDSV_DispatchThreadIDSV_GroupIndexとなります。

今着目しているスレッドが実行される際に、それぞれのセマンティクスを指定した引数にどんな値が渡ってくるか、を示しているのが図の意味なんですね。

SV_GroupThreadIDは、実行しているグループ内でのスレッドIDです。なのでそのまま(7,5,0)なわけですね。
SV_GroupIDは、スレッド群を管理しているスレッドグループのIDなので(2,1,0)となります。

そして若干厄介なのがSV_DispatchThreadIDでしょう。
これは、Dispatchと名前がついている通り、現在実行中のスレッドIDを一意に特定できる情報が格納されます。
つまり、この値を参照すれば、どのスレッドグループのどのスレッドが今実行されているのかが分かる、というわけです。

そして図が説明してくれているのはこのIDの計算方法です。
計算方法を抜粋すると以下のようになっています。

([(2,1,0) * (10,8,3)] + (7,5,0)) = (27,13,0)

つまり、カーネルの引数には(27,13,0)という値が渡ってくるというわけです。(ちなみに引数の型はint3

上の例では3次元なので若干むずかしいですが、簡単のために2 * 2次元のスレッドグループの、4 * 4スレッドを実行した例の図を作成してみました。
以下の図を見てみてください。

f:id:edo_m18:20170509160736p:plain

上の図は、左から順に、「スレッドグループ番号」「スレッドグループごとのスレッドID」「Dispatch Thread ID」の順に、どういう値が割り振られるかを示したものです。
上段にある拡大図は、SV_DispatchThreadIDがどう計算されるかのサンプルを示したものです。

最後の「SV_DispatchThreadID」を見てもらうと分かりますが、うまくX,Y軸の値が連続して並んでいますね。

1行のスレッド数は、「X軸のスレッド数 x X軸のスレッドグループ」となります。
それが、「Y軸のスレッド数 x Y軸のスレッドグループ」の数だけ繰り返されるわけです。

あれ? これを見てなにかに似ていると思わないでしょうか。

そう、テクスチャを二次元配列にしたときの添字と一致しているのが分かるかと思います。
これが、冒頭で書いた「テクスチャなどの多次元配列へのアクセスを考慮したものだ」という話につながります。

スレッド数とスレッドグループ数を多次元で表現すると、こうしたデータへのアクセスがとても容易になるのが分かってもらえたかと思います。

詳細は次回にまわしますが、テクスチャへアクセスするためのコード断片をお見せしましょう。

#pragma kernel CSMain

RWTexture2D<float4> texCopy;
Texture2D<float4> tex;

[numthreads(8,8,1)]
void CSMain(uint2 id : SV_DispatchThreadID)
{
    float4 t = tex[id];
    texCopy[id] = t;
}

今は細かいところは置いておいて、カーネル関数の中身だけに着目してください。
テクスチャへの添字アクセスで、SV_DispatchThreadIDをそのまま使っていることに気づいたでしょうか。

前述のように、SV_DispatchThreadIDは、うまく二次元配列の添字として機能してくれるので、値を加工することなくそのまま使えている、というわけですね。

今回は、ComputeShaderを使う上で大事な「スレッドの概念」についてまとめました。
ComputeShaderを使ってテクスチャを利用して様々な計算が行えるので、次はテクスチャとバッファを利用した計算について書きたいと思います。

*1:カーネルは「核」を意味するので、計算の核となる関数のこと

HTC Viveの埋め込みカメラの映像をテクスチャとして取得・表示する

概要

HTC Viveのヘッドマウントディスプレイには、フロントカメラが埋め込まれています。
このカメラは、メニュー表示時にカメラを起動し外の状況を確認したり、ゲーム画面自体にオーバーレイで周りの状況を表示して、HMDを脱がなくても色々とできるように、という配慮がなされています。

カメラ自体は通常のWebカメラと違いはないので、Unityからカメラの映像をテクスチャとして取り込み、それを利用することができます。

WebCamTextureでも取得はできるようなのですが、SteamVRからもAPIが提供されているので、そちらを利用する方法をメモしておこうと思います。

なお、今回の記事はこちらのValveのコードを参考にしています(というかほぼコピー)。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ViveCam : MonoBehaviour
{
    [SerializeField]
    private bool _undistorted = true;

    [SerializeField]
    private bool _cropped = true;

    [SerializeField]
    private bool _followTrakking = false;

    [SerializeField]
    private Transform _target;

    [SerializeField]
    private Material _material;

    #region ### MonoBehaviour ###
    private void OnEnable()
    {
        EnableSteamVRCamera();
    }

    private void OnDisable()
    {
        DisableSteamVRCamera();
    }

    private void Update()
    {
        UpdateCameraTexture();
    }
    #endregion ### MonoBehaviour ###

    private void UpdateCameraTexture()
    {
        var source = SteamVR_TrackedCamera.Source(_undistorted);
        var texture = source.texture;

        if (texture == null)
        {
            return;
        }

        _material.mainTexture = texture;

        float aspect = (float)texture.width / texture.height;

        if (_cropped)
        {
            var bounds = source.frameBounds;
            _material.mainTextureOffset = new Vector2(bounds.uMin, bounds.vMin);

            float du = bounds.uMax - bounds.uMin;
            float dv = bounds.vMax - bounds.vMin;

            _material.mainTextureScale = new Vector2(du, dv);

            aspect *= Mathf.Abs(du / dv);
        }
        else
        {
            _material.mainTextureOffset = Vector2.zero;
            _material.mainTextureScale = new Vector2(1f, -1f);
        }

        _target.localScale = new Vector3(1f, 1f / aspect, 1);

        if (_followTrakking)
        {
            if (source.hasTracking)
            {
                var t = source.transform;
                _target.localPosition = t.pos;
                _target.localRotation = t.rot;
            }
        }
    }

    private void EnableSteamVRCamera()
    {
        var source = SteamVR_TrackedCamera.Source(_undistorted);
        source.Acquire();

        // カメラが認識されていなかったらdisableにする
        if (!source.hasCamera)
        {
            enabled = false;
        }
    }

    private void DisableSteamVRCamera()
    {
        _material.mainTexture = null;

        var source = SteamVR_TrackedCamera.Source(_undistorted);
        source.Release();
    }
}

ポイントは、SteamVR_TrackedCameraを使って該当カメラを有効化し、さらに有効な場合に、Updateメソッド内で、カメラの映像をTextureとして受け取ってそれを更新する、というものです。
外部カメラからの映像をなにかに使ったり、あるいはゲーム中の安全確保のために、一定距離近づいたら外部カメラ有効化してそれを表示する、みたいな使い方もできるかもしれません。

ということで、使い方を知りたかったのでメモとして書きとどめておきます。

UnityでPivotを指定して同一スケーリングを行うスクリプト

https://i.gyazo.com/50ceb29458b165477af349d93fc7fcc5.gif

概要

Unityでは標準ではスケールのPivot位置を変更することができません。
とはいえそうした要望はきっと多いと思います。
実際にググって見ると、モデリングツールで変更しろ、だとか、Empty Objectを作って子にしてからスケーリングしろ、だとか、色々な情報が出てきます。

が、やはりスクリプトからPivot位置を指定してスケールを掛けたい場合もきっとあると思います。
そのために階層構造をいじるのはなんか違う気がしますし、なにより気持ち悪いです。

ということで、スクリプトからそれを実現できないかと思い、色々といじってみてうまく行ったのでそれを書きとどめたいと思います。

※ ただし、今回の例ではスケールに利用する値はすべて同じ値を想定しています。そうでない場合はSkewの影響でx,y,zの3要素で取得できないため別の処理が必要になります。

どのように動くかは冒頭のアニメーションGifを見てみてください。
ちょっと分かりづらいかもしれませんが赤いGizmoがPivotです。

コード

それほど長くないので、まずはコード全文を載せます。
コードは以下のような感じで実装しました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class PivotScaling
{
    /// <summary>
    /// ターゲットのTransformを、Pivotを基点にスケーリングする
    /// </summary>
    /// <param name="target">対象Trasnform</param>
    /// <param name="pivot">スケーリング基点</param>
    /// <param name="scale">スケーリング値</param>
    public static void Scale(Transform target, Vector3 pivot, float scale)
    {
        // 指定スケールになるように元のスケールを求める
        Vector3 originalScale = GetScale(target.localToWorldMatrix);

        // 親のマトリクス(Pivot用)
        Matrix4x4 parentMat = Matrix4x4.identity;
        parentMat[0, 0] = scale / originalScale.x;
        parentMat[1, 1] = scale / originalScale.x;
        parentMat[2, 2] = scale / originalScale.x;

        parentMat[0, 3] = pivot.x;
        parentMat[1, 3] = pivot.y;
        parentMat[2, 3] = pivot.z;

        // 自身のマトリクス
        Matrix4x4 mat = target.localToWorldMatrix;

        // 親の原点からの相対位置に移動させる
        Matrix4x4 transMat = Matrix4x4.identity;
        transMat[0, 3] = -pivot.x;
        transMat[1, 3] = -pivot.y;
        transMat[2, 3] = -pivot.z;

        Matrix4x4 result = parentMat * transMat * mat;

        target.position = GetPosition(result);
        target.localScale = GetScale(result);
        target.rotation = GetRotation(result);
    }

    /// <summary>
    /// マトリクスから位置を取り出す
    /// </summary>
    static Vector3 GetPosition(Matrix4x4 matrix)
    {
        return matrix.GetColumn(3);
    }

    /// <summary>
    /// マトリクスの回転行列からQuatenionを取り出す
    /// </summary>
    static Quaternion GetRotation(Matrix4x4 matrix)
    {
        return Quaternion.LookRotation(matrix.GetColumn(2), matrix.GetColumn(1));
    }

    /// <summary>
    /// マトリクスからスケールを取り出す
    /// 
    /// @warning ただし、uniform-scaleのみに対応
    /// </summary>
    static Vector3 GetScale(Matrix4x4 matrix)
    {
        float x = Mathf.Sqrt(matrix[0, 0] * matrix[0, 0] + matrix[0, 1] * matrix[0, 1] + matrix[0, 2] * matrix[0, 2]);
        float y = Mathf.Sqrt(matrix[1, 0] * matrix[1, 0] + matrix[1, 1] * matrix[1, 1] + matrix[1, 2] * matrix[1, 2]);
        float z = Mathf.Sqrt(matrix[2, 0] * matrix[2, 0] + matrix[2, 1] * matrix[2, 1] + matrix[2, 2] * matrix[2, 2]);
        return new Vector3(x, y, z);
    }
}

主な部分は最初のScaleメソッドだけです。
ここで行っていることは、手動でEmpty Objectを作って操作していることをコードでやっているに過ぎません。

大まかになにをしているかと言うと、以下のような手順で計算を行っています。

  1. 親のマトリクス相当の計算(スケールと平行移動)(*1)
  2. 対象Transformの座標を、親空間での位置に変換するための行列生成
  3. それらをかけ合わせて最終的な結果を取得

といった流れです。

最初のところでは、親となるEmpty Object相当のマトリクスを生成しています。
冒頭でも書いたように、同じ値のスケール(uniform scaling)以外の場合は結果が異なってしまうため想定していません。
(とはいえ、スケールの成分がそれぞれ異なるというケースはそこまで多くないと思います)

そのため、引数で受け取るのはfloatのスケール値です。
それを、現在のスケール値で割っているのは、最終的な計算結果のスケールが、指定されたスケールになるようにするためです。
(じゃないと、同じスケール値を指定してもどんどん拡大していってしまう)

parentMatのスケールを設定してるのは対角成分です。
スケールは以下のように対角部分に現れるので、それを直接書き込んでいるわけですね。
行列表現をすると以下のようになります。

\begin{vmatrix} S_x & 0 & 0 & 0 \\ 0 & S_y & 0 & 0 \\ 0 & 0 & S_z & 0 \\ 0 & 0 & 0 & 1 \end{vmatrix}

その後、親の平行移動成分を書き込んでいます。いわゆるPivot位置ですね。
平行移動成分は最後の行に格納されるため、[n, 3]要素に書き込んでいます。
このあたりは座標変換行列の基本ですね。

\begin{vmatrix} 0 & 0 & 0 & T_x \\ 0 & 0 & 0 & T_y \\ 0 & 0 & 0 & T_z \\ 0 & 0 & 0 & 1 \end{vmatrix}

※ 行列は、扱うシステムによってオーダーが異なります(行オーダー/列オーダーの2種類)。Unityの場合は上記のように「列」となります。

親の変換行列ができたら、次に生成している行列は、「親空間での座標位置への変換行列」です。
Empty Objectを生成して実行する場合は、Unityが自動的に座標変換してくれます。
実際にヒエラルキー状でドラッグ&ドロップすると、位置は変わらないものの、インスペクタの表示は変わりますね。
それを表現しているというわけです。

そして最後は、求めた各行列を、対象のTransformから得られた現在の行列に掛けてあげるだけです。

Matrix4x4 result = parentMat * transMat * mat;

これで求めたい行列が求まりました。

ただ、(なぜか)残念なことに、Transformに直接マトリクスを設定する方法がないため、行列から「位置」「回転」「スケール」を個別に取り出して設定しています。
それを行っているのが、サポート的に実装したGet****メソッドたちです。

サポートメソッド

それぞれのサポートメソッドが行っている処理は以下のようになっています。

GetPosition

行列から平行移動要素を取り出します。
といっても、設定と同じく3カラム目(0始まりなので4番目)の値をそのままベクトルとして抽出するだけです。

/// マトリクスから位置を取り出す
/// </summary>
static Vector3 GetPosition(Matrix4x4 matrix)
{
    return matrix.GetColumn(3);
}

平行移動要素はとてもシンプルですね。

GetRotation

続いて回転。
回転の取り出しは、行列から、オブジェクトの「ワールド空間での」各軸の方向が取り出せることを利用して、Quaternion.LookRotationを用いてクォータニオンを取得します。

/// <summary>
/// マトリクスの回転行列からQuatenionを取り出す
/// </summary>
static Quaternion GetRotation(Matrix4x4 matrix)
{
    return Quaternion.LookRotation(matrix.GetColumn(2), matrix.GetColumn(1));
}

matrix.GetColumn(2)で2番目のカラムを取り出していますが、これが、ワールド空間でのforwardのベクトル(非正規化)になります。
同様に、matrix.GetColumn(1)upベクトルに相当するため、このふたつを用いてクォータニオンを生成しているわけです。

GetScale

最後にスケール。
行列をある程度知っている人であれば、回転行列とスケールは掛け合わされて保存されていることを知っていると思います。
なので、値としてはとても複雑な値になってしまっています。
果たしてここから目的の値を取り出せるの? と思うかもしれませんが、可能です。

というのも、回転行列の値は、スケールが1であれば単位ベクトルを回転したものになっています。
裏を返せば、単位ベクトルになっていない=スケールがかかっている、というわけです。

と、ちょっと長く書きましたが、要は該当要素の「長さ」を計算してやることで、まさに「スケール」が求められる、というわけなのです。
実際のメソッドの中の処理は以下のようになります。

/// <summary>
/// マトリクスからスケールを取り出す
/// 
/// @warning ただし、uniform-scaleのみに対応
/// </summary>
static Vector3 GetScale(Matrix4x4 matrix)
{
    float x = Mathf.Sqrt(matrix[0, 0] * matrix[0, 0] + matrix[0, 1] * matrix[0, 1] + matrix[0, 2] * matrix[0, 2]);
    float y = Mathf.Sqrt(matrix[1, 0] * matrix[1, 0] + matrix[1, 1] * matrix[1, 1] + matrix[1, 2] * matrix[1, 2]);
    float z = Mathf.Sqrt(matrix[2, 0] * matrix[2, 0] + matrix[2, 1] * matrix[2, 1] + matrix[2, 2] * matrix[2, 2]);
    return new Vector3(x, y, z);
}

ただ、お気づきの方もいると思いますが、それぞれの要素の長さを計算しているわけですが、各要素が「個別に」スケールが書けられていた場合、不可逆な値になってしまっています。
これが、各スケールが同じでないとならない理由です。

しかしUnityにはlossyScaleというプロパティがあります。
これは、この回転とスケールの問題を「ある程度」予測して計算を行い、「それっぽい」値を返してくれるプロパティです。
(なので場合によっては誤差が出る)

いちおう計算する方法はあるようなのですが、かなり複雑なので今回は調べるのを断念しました。
そもそも今回の実装はuniformなスケールだけのケースで利用する目的だったため必要なかったのもあります。
とはいえ、uniformスケールであれば問題なく使えるので有用だと思います。

異空間から転送されてきたように演出するマスクシェーダ

概要

今作っているコンテンツで、なにもない空間からオブジェクトが転送されてきたような演出をしたいと思い、そのために色々シェーダを書いたのでそのメモです。
以下の画像を見てもらうとどういう効果かイメージしやすいと思います。

https://i.gyazo.com/e357fe60d5927ed0c1eec2e97da3f644.gif

今回はこれを実装するにあたって色々ハマったり勉強になったりした点を書いていきたいと思います。

なお、こちらで解説しているシェーダなどについてはGithubで公開しているので動作を見たい方はそちらをダウンロードして確認ください。

github.com

まずは方針決め

Deferred Shadingだと、最初にMeshをレンダリングして、その後にBoolean演算を行ってくり抜く、みたいなことができるようです。
(↓こんな感じの。みんな大好き凹みTips)

tips.hecomi.com

ただ、作っているコンテンツがVRなのでDeferredとは相性が悪く、Forward Renderingで行うために、今回はステンシルバッファを利用してレンダリングするように挑戦してみました。
(まぁ結果としてはPassが増えて、そもそも重そうになってしまったのでこれを採用するかは未知数ですが・・;)

方針

方針は、前述のように「ステンシルバッファ」を利用して、マスク対象のオブジェクトの「背面だけ」をどうにかしてレンダリングするようにします。
図で言うと以下の部分ですね。

f:id:edo_m18:20170307023731p:plain

最初に考え始めたときは、カリングやら深度テストやらをごにょごにょすればすぐだろーくらいの感じで考えていたのですが、これがなかなかどうして、色々と考慮しないとならないことが多く、地味にハマりました;
が、おかげでだいぶ深度テストとステンシルバッファの扱いのイメージがしっかりとついた気がします。

基本は3マテリアル

今回のサンプルは3つのマテリアル(=3つのシェーダ)を作成しました。
ただ、各々のシェーダ内で複数パス利用しているので、全体ではだいぶパスが増えています。

登場人物としては「マスクオブジェクト」とマスク対象の「ターゲットオブジェクト」のふたつ。

各シェーダが連携しながらうまくマスクデータを作るようにしています。
レンダリングの順番としては、

  1. マスクオブジェクトをレンダリングして、マスクエリアにマークを付ける ... Mask.shader *1
  2. ターゲットオブジェクトをレンダリングして、ターゲットオブジェクトのクリップ面とそれ以外を分ける ... TargetMash.shader
  3. マスクオブジェクト側で、クリップ面(断面)をレンダリング ... MaskRender.shader
  4. マスクオブジェクトの表面を、ColorMask 0レンダリング(つまり深度値のみ書き込み) ... MaskRender.shader
  5. (2)で収集した「マスクオブジェクトの、カメラから見てマスクオブジェクトから隠れている部分」以外の部分のDepthをクリア ... MaskRender.shader

という手順でレンダリングを行い、マスクエリアを特定します。

マスクエリアを定義する「マスク用シェーダ」

これは単純に、マスクオブジェクトの領域を示すStencil値を書き込み、あとで範囲を限定するために利用します。
コードはとてもシンプルです。

Shader "Custom/Mask" {
    Properties {
        //
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent+10" }

        LOD 200

        Pass
        {
            Stencil
            {
                Ref 10
                Comp Always
                Pass Replace
            }

            Cull Front
            ZWrite On
            ZTest LEqual
            ColorMask 0

            CGPROGRAM

            #include "Assets/Mask/Utility/Mask.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            float4 frag(v2f i) : SV_Target
            {
                return half4(0, 1, 0, 1);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

見ての通り、ColorMask 0にしてマスクエリアにStencil値と深度値を書き込んでいるのみです。
ただ注意点はCull Frontを指定してマスクエリアの「内側」をレンダリングしている点です。

ターゲットをマスクする「ターゲット用シェーダ」

次に、ターゲットとなるオブジェクトを数回レンダリングし、表面と背面をそれぞれ描き分けてマスクを生成します。

f:id:edo_m18:20170307100656p:plain

それぞれマスクをかけて、Viewerで色を表示したところ。
(Viewerはそれ用のシェーダを書いて(後述)、Stencilの値によって色を出力するPlaneを配置しているだけです)

赤い部分が一番最初にマスクを掛けた部分。(つまりマスク対象エリア)
青い部分が、マスクオブジェクトの「内側」に存在するターゲットオブジェクトの部分。
緑色の部分が、ちょうどマスクオブジェクトと交差しているクリップ面(断面)。
そして(やや見づらいですが)グレーの部分が、マスクオブジェクト外だけれどカメラの視点から見るとマスクオブジェクトの向こう側(つまり深度テストに合格していないところ)となります。

このシェーダは全部で3passを利用してマスクを生成しています。
(すべてのpassは、最初のマスクエリアとしてマークした中でのみ処理を行うようにしています)

1pass目

Pass
{
    Stencil
    {
        Ref 10
        Comp Equal
        ZFail IncrSat
    }

    Cull Back
    Zwrite Off
    ZTest GEqual
    ColorMask 0

    CGPROGRAM

    #include "Assets/Mask/Utility/Mask.cginc"
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0


    float4 frag(v2f i) : SV_Target
    {
        return half4(1, 0, 0, 1);
    }

    ENDCG
}

1pass目は、ターゲットオブジェクトの「表面」を、ZTest GEqualレンダリングします。
加えてStencilはZFail IncrSatを指定します。

最初にマスクエリアとしてマークした部分の中で、ZTestを反転した上で、さらにそれがFailした部分にのみマークをつけています。
上でも書いた通り、マスクエリアのレンダリングは、マスクエリアの「内側」をレンダリングしたものでした。

つまり、結果として「マスクエリア内」にある領域に、ステンシルバッファが書き込まれることになります。

f:id:edo_m18:20170307131105p:plain

1pass目までをレンダリングした結果のStencilの状態を表示したところ。青い部分が該当箇所。

2pass目

Pass
{

    Stencil
    {
        Ref 11
        Comp Equal
        ZFail IncrSat
    }

    Cull Front
    Zwrite Off
    ZTest LEqual
    ColorMask 0

    CGPROGRAM

    #include "Assets/Mask/Utility/Mask.cginc"
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0


    float4 frag(v2f i) : SV_Target
    {
        return half4(1, 0, 0, 1);
    }

    ENDCG
}

続いて2passは、1pass目で増加したステンシル値と比較し、同じ箇所に対してレンダリングを行います。
ただ2pass目はZTestを元に戻し、かつCull Frontにしてレンダリングを行い、さらにその上でZFailしたところにマークを付けます。
要は、ターゲットオブジェクトが描かれるべき場所に対して裏側をレンダリングし、かつ「背面」となる部分にマークを付けるわけです。

f:id:edo_m18:20170307131529p:plain

それを実行して、各ステンシルの値ごとに色を塗ると上記のようになります。
この時点で、ターゲットオブジェクトの「背面」に対してマスクが生成されているのが分かるかと思います。

3pass目

Pass
{
    Stencil
    {
        Ref 10
        Comp Equal
        ZFail DecrSat
    }

    Cull Back
    Zwrite Off
    ZTest LEqual
    ColorMask 0

    CGPROGRAM

    #include "Assets/Mask/Utility/Mask.cginc"
    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0


    float4 frag(v2f i) : SV_Target
    {
        return half4(1, 0, 0, 1);
    }

    ENDCG
}

そして最後の3pass目。
最後はまた、Stencilの値が最初にマスクした部分に対して実行し、Cull BackかつZTest LEqualレンダリングします。(つまり普通のレンダリング

その際、これまたZFailした箇所に対して、今度はステンシルの値を減らします。
ここまでを実行すると、最終的には以下のようなマスクの状況になります。

f:id:edo_m18:20170307131852p:plain

最後に、3passを含んだすべてのシェーダコードを載せておきます。

Shader "Custom/TargetMask" {
    Properties
    {
        //
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent+11" }

        LOD 200

        // --------------------------------------------------
        Pass
        {
            Stencil
            {
                Ref 10
                Comp Equal
                ZFail IncrSat
            }

            Cull Back
            Zwrite Off
            ZTest GEqual
            ColorMask 0

            CGPROGRAM

            #include "Assets/Mask/Utility/Mask.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0


            float4 frag(v2f i) : SV_Target
            {
                return half4(1, 0, 0, 1);
            }

            ENDCG
        }


        // --------------------------------------------------
        Pass
        {

            Stencil
            {
                Ref 11
                Comp Equal
                ZFail IncrSat
            }

            Cull Front
            Zwrite Off
            ZTest LEqual
            ColorMask 0

            CGPROGRAM

            #include "Assets/Mask/Utility/Mask.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0


            float4 frag(v2f i) : SV_Target
            {
                return half4(1, 0, 0, 1);
            }

            ENDCG
        }


        // --------------------------------------------------
        Pass
        {
            Stencil
            {
                Ref 10
                Comp Equal
                ZFail DecrSat
            }

            Cull Back
            Zwrite Off
            ZTest LEqual
            ColorMask 0

            CGPROGRAM

            #include "Assets/Mask/Utility/Mask.cginc"
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0


            float4 frag(v2f i) : SV_Target
            {
                return half4(1, 0, 0, 1);
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

マスクエリアをレンダリングする「マスク用レンダリングシェーダ」

さて、以上で今回のサンプルで利用するマスク情報が手に入りました。次は、実際にマスクエリアをレンダリング(カラー出力)し、見た目を構築していきます。

1pass目

Pass
{
    Stencil
    {
        Ref 12
        Comp Equal
    }

    CGPROGRAM

    #pragma vertex vert
    #pragma fragment frag

    float4 frag(v2f i) : SV_Target
    {
        return _MaskColor;
    }

    ENDCG
}

1pass目はとてもシンプルです。
Stencil値が12のエリアに対してレンダリングを行います。
Stencil値が12の箇所は「ターゲットオブジェクトの背面」部分です。

なので、ここはクリップされた断面を出力するパスになります。
これを実行すると以下のようになります。

f:id:edo_m18:20170307132605p:plain

まだ他のパスを描いていないので若干分かりづらいかもしれませんが、断面にだけ、指定した色が塗られているのが分かるかと思います。

2pass目

Pass
{
    Blend SrcAlpha OneMinusSrcAlpha
    Cull Back
    
    CGPROGRAM

    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0

    float4 frag(v2f i) : SV_Target
    {
        return half4(0, 0.5, 1.0, 0.0);
    }

    ENDCG
}

2pass目は、マスクオブジェクトの「表面」をレンダリングします。
ただレンダリングといっても完全透明になるようにレンダリングするため、ぱっと見はなにが起きたか分からないかもしれません。
(そして同時に、深度値も更新しています。というか、深度値を更新することが主な目的)

それを実行すると、以下のように、ターゲットオブジェクトがマスクオブジェクトの背面に隠れて消えるのが分かるかと思います。

f:id:edo_m18:20170307132741p:plain

3pass目

Pass
{
    Stencil
    {
        Ref 9
        Comp Equal
        Pass Keep
    }

    Blend SrcAlpha OneMinusSrcAlpha
    Cull Back
    ZTest Always
    
    CGPROGRAM

    #pragma vertex vert
    #pragma fragment frag
    #pragma target 3.0

    FragOut frag(v2f i)
    {
        FragOut o = (FragOut)0;
        o.color = half4(0.0, 0.5, 1.0, 0.0);
        o.depth = 1 - UNITY_NEAR_CLIP_VALUE;
        // #if UNITY_REVERSED_Z
        // o.depth = 0;
        // #else
        // o.depth = 1;
        // #endif
        return o;
    }

    ENDCG
}

最後の3pass目です。
ここは少しだけ特殊な処理が入っています。

まず、このパスに関してはZTest Alwaysレンダリングを行います。
かつ、ステンシルの値は、ターゲットオブジェクトでマスクのデータを収集した際に「減算」した部分のみに行います。

そしてその箇所の「深度値をクリア」します。
該当コードは以下の部分です。

o.depth = 1 - UNITY_NEAR_CLIP_VALUE;

この後の補足で書きますが、プラットフォームごとに深度値の扱いが変わるため、それを考慮した記述になっています。
やっていることはシンプルに、深度値を一番遠い部分(つまりまだなにも描かれてない状態)に初期化します。

なぜそうするかというと、2pass目で深度値を描いてしまっているがために、ターゲットとなるオブジェクトが透明なマスク領域に「隠れて」しまうため、そこの部分をくり抜くために実行しているわけです。

それを踏まえて実行すると、以下のように冒頭のアニメーションGifと同じ見た目になるのが確認できます。

f:id:edo_m18:20170307133025p:plain

ターゲットとなるオブジェクトは、最終的なパスで普通にオブジェクトをレンダリングしてやれば無事、マスク領域でクリップされた断面が描かれるようになる、というわけです。

このシェーダの全文も以下に載せておきます。

Shader "Custom/MaskRender" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MaskColor ("Mask color", Color) = (0.0, 0.9, 1.0, 1.0)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent+12" }

        LOD 200


        // --------------------------
        CGINCLUDE

        sampler2D _MainTex;
        sampler2D _MaskGrabTexture;

        struct appdata
        {
            float4 vertex : POSITION;
            float3 normal : NORMAL;
        };

        struct v2f
        {
            float4 pos : SV_POSITION;
            float3 normal : TEXCOORD1;
            float4 uvgrab : TEXCOORD2;
        };

        struct FragOut
        {
            float4 color : SV_Target;
            float depth : SV_Depth;
        };

        fixed4 _Color;
        fixed4 _MaskColor;

        v2f vert(appdata i)
        {
            v2f o;
            o.pos = mul(UNITY_MATRIX_MVP, i.vertex);

            #if UNITY_UV_STARTS_AT_TOP
            float scale = -1.0;
            #else
            float scale = 1.0;
            #endif

            // Compute screen pos to UV.
            o.uvgrab.xy = (float2(o.pos.x, o.pos.y * scale) + o.pos.w) * 0.5;
            o.uvgrab.zw = o.pos.zw;

            return o;
        }

        ENDCG
        // --------------------------


        // ------------------------------------------------------
        // Target back face.
        Pass
        {
            Stencil
            {
                Ref 12
                Comp Equal
            }

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            float4 frag(v2f i) : SV_Target
            {
                return _MaskColor;
            }

            ENDCG
        }


        // ------------------------------------------------------
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Back
            
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            float4 frag(v2f i) : SV_Target
            {
                return half4(0, 0.5, 1.0, 0.0);
            }

            ENDCG
        }


        // ------------------------------------------------------
        Pass
        {
            Stencil
            {
                Ref 9
                Comp Equal
                Pass Keep
            }

            Blend SrcAlpha OneMinusSrcAlpha
            Cull Back
            ZTest Always
            
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            FragOut frag(v2f i)
            {
                FragOut o = (FragOut)0;
                o.color = half4(0.0, 0.5, 1.0, 0.0);
                o.depth = 1 - UNITY_NEAR_CLIP_VALUE;
//              #if UNITY_REVERSED_Z
//              o.depth = 0;
//              #else
//              o.depth = 1;
//              #endif
                return o;
            }

            ENDCG
        }
    }
    FallBack "Diffuse"
}

番外編 - Stencil Viewer

解説で書いていた「Stencil Viewer」ですが、こちらの記事(Unity 4.2 - Stencils for portal rendering)で紹介されているコードを読んでいたときに知ったものです。

といっても大した内容ではなく、以下のように、できるだけレンダリング順を後に回し、該当のステンシル値が書き込まれたエリアに対して「確実に」色を出力するシェーダを書きます。

Tags { "RenderType"="Opaque" "Queue"="Transparent+500"}      

ZWrite Off
ZTest Always

fixed4 _Color0;

Pass
{
    Stencil 
    {
        Ref 9
        Comp Equal
        Pass Keep
    }

    CGPROGRAM

    #pragma vertex vert
    #pragma fragment frag

    half4 frag(v2f i) : COLOR 
    {
        return _Color0;
    }

    ENDCG
}

まず、Tags"Queue"="Transparent+500"を追加して、レンダリング順を透明オブジェクトよりもさらに後に回します。
その上で、ZTest Alwaysに変更して、深度テストを無視してステンシルのみでレンダリングが判断されるようにします。

あとはステンシルの参照値を設定してそこに対して色を出力すれば、冒頭の説明のキャプチャのように、ステンシル値ごとに塗り分けることができる、というわけです。
(複数のステンシル値の色を出力したい場合は上記のPassを複数記述して、それぞれ参照するステンシル値を変更してあげればOKです)

出会った問題点

深度

プラットフォームごとの深度値の扱い

今回のサンプルでは、上の深度テストでの問題でも触れたように、オブジェクトごとの重なり以外にもカメラからの視点方向によるマスキングなどが行われ、色々と試行錯誤しました。
最終的には深度値をシェーダ側でクリアすることによって対策したのですが、その際に知った点として、バッファで使われている深度値がプラットフォーム(OpenGLDirectXPS4など)ごとに異なる、という点でした。

以下のUnityのドキュメントにも記載があり、またそれぞれのプラットフォーム向けに適切にコンパイルされるようマクロなども用意されています。

https://docs.unity3d.com/Manual/SL-PlatformDifferences.htmldocs.unity3d.com

サンプルでは、マスクした上でマスク対象以外のDepthをクリアしています。該当コードを抜き出すと以下。

FragOut frag(v2f i)
{
    FragOut o = (FragOut)0;
    o.color = half4(0.0, 0.5, 1.0, 0.0);
    o.depth = 1 - UNITY_NEAR_CLIP_VALUE;
    // 下の書き方でも多分大丈夫
    // #if UNITY_REVERSED_Z
    // o.depth = 0;
    // #else
    // o.depth = 1;
    // #endif
    return o;
}

Unity側でどちらのタイプか、あるいはNear Clip面の値を持つdefineがあるので、それを利用して適切に値を設定しています。
(やっていることは、該当の箇所のDepthを、なにも描かれていない状態(=一番遠い値)にしています)

Tagsの「Queue」はひとつのシェーダ内でひとつのみ有効

最初、TagsQueueでレンダーキューをいじってひとつのシェーダ内でごにょごにょしようとしていたら、QueueSubShaderにのみ適用されて、Passには影響しないことを知りませんでした。(Passそれぞれに影響するTagsもあります)
ただ、Pass自体は書かれた順に実行されるので、Queueの中でレンダリング順を制御する場合にはPassの順番を入れ変えることで順番を制御することができます。
@hecomiさんに指摘されて追記しました)

調べてみたら以下の投稿同じ質問をしている人がいました。

answers.unity3d.com

そこでの回答が以下。

The reason the code above doesn't work is that "tags" apply only to subshaders, whereas culling/depth testing options apply to passes.

参考にした記事

*1:今回作成したシェーダ名