テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】IJobParallelForTransformでTransformを並列処理する際でも、スレッドをフル活用する為のポイント

f:id:tsubaki_t1:20181114222538g:plain

今回はC# Job SystemでTransformAccessArrayを利用した際の、ジョブの分割数についてのお話です。

TransformAccessArrayが並列処理されない?

TransformAccessArrayは現行のGameObjectベースの処理を並列処理する上で非常に便利な機能です。上手く利用することで、Transformの座標取得や更新(実は意外とコストが高い)といった項目を別スレッドに逃がす事が期待できます。

さて以前に TransformAccessArrayIJobParallelForTransform の組み合わせでについての記事を書いたのですが、その際にジョブが必ず一つしか発行されていませんでした。
そうなると、単純にTransformを他の NativeArray<float3> に移すとかなら兎も角、普通に計算して云々すると一本に時間が集約されすぎて余り良くありません。Transformの結果を使うなり、結果を書き込むなりする場合でも、 処理完了まで詰まる 事になります。

f:id:tsubaki_t1:20181114224421j:plain

親が同じならば単一のジョブで実行される

回避方法ですが、非常に単純なものでした。端的にいえば Transformの親が同じならば単一のジョブで実行される というものです。 例えば上のジョブの実行結果ですが、アクセスを簡略化するためParentを用意し、 GetComponentInChildren を利用してアクセスしていました。

これを、transform.DetachChildren()で親子構造を解除してやった所、ちゃんと並列で動くようになりました。 処理はオブジェクト単位で実行されるので、処理が完了するまでの時間は短くなっています。

f:id:tsubaki_t1:20181114224629j:plain

f:id:tsubaki_t1:20181114224638j:plain

f:id:tsubaki_t1:20181114224432j:plain

ジョブの分割数を制限

ただし良いことばかりではありません。ジョブがコアを全て使って処理することは喜ばしい事ですが、ジョブは増えれば増えるほどに処理の効率は下がりますし、ジョブを発行する処理の時間が増加します。 例えばジョブが一つしか発行されていなかった時のジョブ発行は 0.015ms程度で収まっていたのですが、ジョブの分割数を上げる事で0.073ms程度まで肥大化しています。 また全体の処理時間も0.7ms程度だったのが1.29まで上がっています。
処理効率は処理完了までの時間とトレードオフ になるので、出来ればその辺りはコントロールしたい所です。

そこでTransformAccessArrayのコンストラクタの第二引数、desiredJobCountを設定してやることでジョブをN分割します。通常のIJobParallelForはバッチ数という…考え方が逆なので、そこんとこ注意です。
なおdesiredJobCountよりもルートの数の方が優先されます。

f:id:tsubaki_t1:20181114225529j:plain

f:id:tsubaki_t1:20181114234943j:plain

gist.github.com

複数のIJobParallelForTransformを発行するとどうなるの?

さて色々と並列処理を試していると、同一のTransformを参照しているTransformAccessArrayに対してIJobParallelForTransformを発行するとどうなるのか興味が出てきます。

結論としては、「同一のTransformを参照しているTransformAccessArrayを参照するIJobParallelForTransform」は、順次実行されます。これはTransformAccessArrayが同一インスタンスへ参照している場合も同様です。 例えば下のように2つのジョブを発行し2つ完了するまで待つ…とすると、片方が終わるまで待機するという形を取ります(2つ目の画像)。下の例ではジョブを一つにまとめていますが、複数に分割した場合は「同じTransformへアクセスするジョブが同時に走らない」だけでジョブはガンガン詰まります(3つ目の画像)。

f:id:tsubaki_t1:20181114231004j:plain

f:id:tsubaki_t1:20181114231240j:plain

f:id:tsubaki_t1:20181114231403j:plain

逆を言えば、同一のTransformAccessAray(ルートのTransform)のオブジェクトはどう頑張っても並列処理されません。そのため、一気に処理を行いたい場合には一旦NativeArray<Vector3>へ格納するようなジョブを発行し、一気に計算、最後に戻す…といった形のジョブを検討しても良いかもしれません。

f:id:tsubaki_t1:20181114233221j:plain

gist.github.com

関連

TransformAccess を使用するタイプです。

kou-yeung.hatenablog.com

ハイブリットECSのCopyTransformFromGameObjectを使用するパターンです。一旦Positionをコピーして最後に書き戻すタイプ

learning.unity3d.jp

前に書いたC# Job Systemの解説ですが、そろそろ書き直す必要があります(書き直しています)

tsubakit1.hateblo.jp

【Unity】NavMeshのRaycastを非同期で撃つ!

http://cdn-ak.f.st-hatena.com/images/fotolife/t/tsubaki_t1/20150925/20150925014548.png

Unity 2018.3からNavMeshのRaycastを非同期で実行することが出来るようになったので、その方法についてです。
なおExperimentalなAPIを使用するので、今後変わるかもしれません。

NavMeshQuery経由で非同期でRaycastを実行

NavMeshの経路探索はNavMeshAgent経由ならば非同期で扱えるのですが、NavMeshAgent無しだと非同期で扱えません。 対してNavMeshQueryを利用したNavMeshに対する操作は、NavMeshAgentほど簡単には扱えませんが、非同期で使用することが出来ます。

そのNavMeshQueryでもRayCastを使用できるようになりました。これはNavMeshQueryを利用したNavMeshAgent自作より手軽に使えるので、覚えておいて損はないかなという感じです。

NavMeshのRayCastについてはこちら。主な用途としては「単純なNPCの視界判定」とかでしょうか。

tsubakit1.hateblo.jp

使ってみる

最も単純な使い方についてです。

最初に事前準備です。NavMeshWorld.GetDefaultWorld();でNavMeshの情報を取得します。確認していませんが、多分、NavMesh再構築したら再度取得しなきゃいけない系のやつです。次にNavMeshQuery query = new NavMeshQuery(world, Unity.Collections.Allocator.Persistent);でqueryの取得です。今回はパス検索しないので使い回すことにしていますが、本当はキャラクターや群体の単位で1queryくらいの扱いになりそうです。

次にLocationを取得します。キャラクターの開始座標はVector3ではなくNavMesh上の座標であるLocationを使用します。こちらも使い回す事も出来ますが、最初は使い捨てです。

あとはRayCastを実行するだけです。

gist.github.com

実行すると、以下の通り。間に障害物があってパスが通らなければTrue、何もなければFalseです。ただしNavMesh外といったパスが通らない場所からスタートした場合もFalseになります。query.Raycast戻り値のPathQueryStatusがSuccessかどうかは確認しといたほうが良いです。

f:id:tsubaki_t1:20181111224009j:plain

f:id:tsubaki_t1:20181111224019j:plain

f:id:tsubaki_t1:20181111224029j:plain

RaycastというよりはLinecastが近いですね

非同期で動かそう!

今度はコレを非同期で動かします。
NavMeshQueryが実行する処理は殆どジョブ上で動作させることが出来るので、やることは上で書いたようなコードをC# Job Systemに移すだけです。

結果は下のような感じになります。メインスレッドの仕事はジョブを発行するまでで、その他の仕事は別スレッドへ移行します。ついでにログの表示もジョブでやらせてるので、本当に殆ど処理時間がなくなっています。

f:id:tsubaki_t1:20181111231034j:plain

gist.github.com

結果をメインスレッドで扱いたい場合は、handle.Complete()と次のジョブを開始する間に処理を記述します。例えばUIに結果を格納したい場合は、下のような感じになります。要するにhandle.Complete()からjob.Schedule(handle)までがメインスレッドからパラメーターにアクセスしても良い時間です。
メインで行う処理が多岐に渡る場合、データを一旦コピーして即スレッド開始という手段も考慮に入れても良いかもしれません。

f:id:tsubaki_t1:20181111233130j:plain f:id:tsubaki_t1:20181111232908g:plain

関連

tsubakit1.hateblo.jp

tsubakit1.hateblo.jp

【Unity】ECSで配列を格納する Dynamic Buffers

今回はECSで配列を使用する方法についてです。

ECSで配列を使用する

ECSに格納するComponentDataには配列を格納出来ません。とはいえ、マネージドなメモリも格納出来るSharedComponentDataは大量の種類を生成するとECSの効率が著しく下がるので余り良くありません。
とはいえ、 "接触したキャラクターの一覧"や"NavMeshのパス"など、連続した(可変の)データを必要とするケースはソコソコ多いです。

今回はDynamic Buffersを使用してこの問題を回避します。
下のような感じで特定のEntityが他のEntityを追跡する機能を作成してみました。中央のシリンダーが他の3つのEntityへの参照を保持していて、その参照経由でEntityへの線を引きます。

f:id:tsubaki_t1:20181107223223g:plain

作ってみる

まず線を引く対象となるEntity群を用意します。コードで配置するのが面倒くさいのでハイブリットです。

GameObjectEntityとPositionComponentを用意します。 Transformの位置とPositionを同期したいのでCopyTransformFromGameObjectでPositionにTransformの座標を毎フレーム登録してもらいます。
今回はこれを青い枠で囲んだオブジェクト全てにセットしています。

f:id:tsubaki_t1:20181107224104j:plain

f:id:tsubaki_t1:20181107224627j:plain

線を引くためにEntityに他のEntityを登録するコードを用意します。

最初にバッファを定義します。IBufferElementDataを継承したstructに含めたい要素を登録します。今回は複数のEntityを登録することが目的なのでEntityです。
設定後はInternalBufferCapacityで配列の長さを設定します。下の図の場合は配列に4つの要素が登録出来ます。
長ければ長いほど、チャンクに格納出来るEntityの数が減ります。オーバーしても良いですが、その場合はヒープメモリとして処理されるらしいです。

f:id:tsubaki_t1:20181107231523j:plain

f:id:tsubaki_t1:20181107231651j:plain

あとはバッファをEntityに追加して、要素を登録します。

アーキタイプから作っている場合はComponentTypeで一発で足せるのですが、ハイブリットはザクっと見た感じバッファを最初から持てなかったので、entityManager.AddBuffer(entity)でバッファを追加しています。
追加後はバッファを取得し、中身を埋めていきます。

f:id:tsubaki_t1:20181107232215j:plain

gist.github.com

あとは線を引くシステムを用意します。

まずグループですが、普通にComponentTypeが使えます。今回の場合EntityBufferを持つEntity一覧を取得しています。

f:id:tsubaki_t1:20181107233217j:plain

次にグループからバッファを取得します。GetBufferArrayで取得するわけですが、Entity単位ではなくグループ単位で持ってきます。
例えばEntityBufferを持つEntityが2つある場合、下のように取得します。

buffer[0] { entity[0], entity[1], entity[2], entity[3] },
buffer[1] { entity[0], entity[1], entity[2], entity[3] },

Reinterpret()は、取得する要素を別の型へ解釈するという機能です*1。内容物が同じレイアウトなら別の型として使用することが出来ます。今回の場合、中身がEntityのみなのでEntityの配列と解釈して処理させています。

f:id:tsubaki_t1:20181107233942j:plain

あとは使用する部分ですが、まんま配列としてアクセス出来ています。 もしパラレルで並列処理したい場合は、ToNativeArrayでバッファをNativeArrayとしてアクセスするのが良いかもしれません。
今回の場合、別スレッドだとEntityから要素を取得できないので、ジョブは使用していません。

f:id:tsubaki_t1:20181107234010j:plain

gist.github.com

サンプル2

Dynamic Bufferに格納した複数の要素を、毎フレーム別スレッドで加算するだけ

gist.github.com

関連

Dynamic Buffersの説明

https://github.com/Unity-Technologies/EntityComponentSystemSamples/blob/master/Documentation/reference/dynamic_buffers.mdgithub.com

*1:実験的な機能

【Unity】SpriteAtlasでパックしたスプライトをUIに使用したときに出るゴミの対処

SpriteAtlasを始めとしたパッキングのアプローチを使用することで、スプライトの描画時に必要なパスの数が減り、パフォーマンス的に良い感じになります。
よく言われるDrawCallやSetPassの削減というやつです。

さて、SpriteAtlasでスプライトをパッキングした後、スプライトをUIに乗せると、下のような謎の絵が紛れ込む事があります。
今回はその対処についてです。

f:id:tsubaki_t1:20181105221418j:plain

 

 

キャラクターの背後に出現した不可解な顔、その正体とは…!?

正体はSpriteAtlasでスプライトをパックした時に詰め込まれた他のスプライトです。幽霊の正体見たり枯れ尾花。
UIは基本的に矩形で切り出されるので、下のようにタイトな設定で詰め込むと矩形の範囲に他のスプライトが入り込み、心霊写真のように他のスプライトが紛れ込んでしまいます。

f:id:tsubaki_t1:20181105222027j:plain

 

対策1:タイトパッキングを外す

この非常に単純な解決法は、UIに使用するスプライトのタイトパッキングを外す事です。タイトパッキングを外せば矩形の余裕を持ってパックするので、UIで取り出した時に他のスプライトが紛れ込む事はありません。

f:id:tsubaki_t1:20181105222349j:plain

 

対策:UIでUse Sprite Meshを使用する

Unity 2018.3から新しい対策が追加されました。UIコンポーネントでUse Sprite Meshを使用することです。

この設定を有効にすると、UIが利用するメッシュが矩形ではなくスプライトのポリゴンを使用するようになり、タイトなパッキングを利用した場合もゴミが出なくなります。

f:id:tsubaki_t1:20181105223045j:plain

当然、表示にかかる頂点数も跳ね上がりバッチの為のコストも上がります。が、特にキャラクターをUIで表示するような場合は「頂点数」より「隙間」が生む透明塗りつぶしの部分の方が問題になるかもしれません。

なお、Use Sprite Meshが使用できるのは「シンプル」のみです。

 

関連

下のをUIで作ると、今回の現象が起こります

tsubakit1.hateblo.jpポリゴンは最初は超高解像度なので、適当に間引いておくと良いです。

tsubakit1.hateblo.jp

SpriteAtlasという機能について

tsubakit1.hateblo.jpUIとSpriteの使い分け

tsubakit1.hateblo.jp