テラシュールブログ

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

【Unity】AssetBundleからモデルをロードする際の最適化ポイント

ゲームでキャラクターをロードしたりする際、画面がカクつくというのは割とよく見る光景です。今回はその辺りの対処法についてメモしておきます。

Async Upload Pipeline(非同期アップロードパイプライン)の活用

ロード時にカクつく要因の一つは、GPUへのアップロードです。これは一度に全てをアップロードするのではなく、分割でアップロード(タイムスライス)することで回避することが出来ます。使用出来るのは以下の設定項目です。

  • 読み込み/書き込み有効無効なテクスチャ(及びミップマップ)
  • 読み込み/書き込み有効無効かつ、メッシュ圧縮オフなモデル
    (※Unity 2018.3以降)

つまりアセットでの設定をしておけば、あとは裏で勝手によしなにしてくれます。 ただしインポート直後のモデル等は大抵ReadWriteがEnableなので、その辺り注意です。ユニティちゃんのモデルもReadWriteが付いてます。

f:id:tsubaki_t1:20181117221858j:plain

f:id:tsubaki_t1:20181117222124j:plain

また使用する上でのパフォーマンスについても、ある程度の調整が効きます。

  • 非同期アップロードタイムスライスの値
    小さいとロードが伸びるがスムーズ
    大きいとロードが短縮されるが処理落ちを起こす
  • 非同期アップロードバッファサイズの値
    一回のロードで扱えるサイズ。小さいと一度に処理できる量が減る

Unity 2018.3以前は、使用するデータの小ささもあったのか「分割数多め」の設定でしたが、Unity 2018.3から非同期アップロードバッファサイズを大きめに設定しています。この設定によって、ロードが倍くらい早くなるという話もあるそうです。

これらの設定は 編集 > 設定 > Quality で確認することが出来る他、スクリプトからも設定することが出来ます。

f:id:tsubaki_t1:20181117215157j:plain

Async Upload Pipelineについては下の記事が正しいです。

blogs.unity3d.com

シェーダーのウォーミングアップ

モデルを表示する際に毎回ガッツリ止めてくれる要因の一つに、シェーダーのパースがあります。処理で言えば、Shader.ParseShader.CreateGPUProgramといったものがそうです。 特にモバイルだとガツガツと止めてきます。

これを回避する為のアイディアは2つです。

  • ドライバにシェーダーを事前にロードしておく
  • ロードしたシェーダーを使い回す

シェーダーを事前にロードする

まずは事前にシェーダーをロードします。
ShaderVariantCollectionに事前ロードしておきたいシェーダー一覧を登録しておき、ShaderVariantCollection .WarmUpで実際にロードするだけです。
指定されたシェーダーは、カクついても問題ないタイミングで読んでおけば、以降カクつく事はありません。

f:id:tsubaki_t1:20181117231406j:plain

全てのシェーダーを登録するとパースに恐ろしい時間がかかるので、必要なものだけを登録しておきます。

ロードしたシェーダーを使い回す

もう一つ、ロードしたシェーダーを使い回します。

ShaderVariantCollectionが事前ロードするシェーダーを、全てShaderVariantCollectionと同じAssetBundleに格納するだけです。

f:id:tsubaki_t1:20181117235229g:plain

というのも、AssetBundleに暗黙的に格納された(AddressやAssetBundle Nameを指定されていない)アセット群は、基本的にAssetBundle毎に独立した存在とみなされるためです。
AssetBundle Browserで確認すると、同じアセットを参照しているものは警告が出ます。

f:id:tsubaki_t1:20181117235012j:plain

同じアセットを別のAssetBundleが暗黙的に格納している場合、コンテンツの中身は完全に一致する”だけ”の、別のアセットとして扱われます。
そのため、今回のケースではShaderVariantCollectionWarmUp()しても、ロードされるのは同じAssetBundle内のシェーダーのみです。他のAssetBundleに格納されているシェーダーは別途ロードされることになります。

f:id:tsubaki_t1:20181117233337j:plain

つまり、今回の理屈は以下のような事です。

  • シェーダーをAssetBundleに明示的に指定することで、他のAssetBundleは明示的に格納したシェーダーを使うようにする。
  • ShaderVariantCollectionとソレが参照するシェーダーを同じAssetBundleに明示的に格納しておくことで、WarmUp()出来るようにしておく

なおShaderVariantCollectionを格納しているAssetBundleを積極的にUnloadすると、AssetBundle間の参照関係やアセット運用周りがおじゃんになるので、そこんとこ注意です。

関連

その他、最適化ポイント云々

tsubakit1.hateblo.jp

アセット設定の一括変更とかは、コレが楽で好きです。

tsubakit1.hateblo.jp

色々試す前に「施工前と施工後」を確認できるようにする。話はそれからだ
Stay alert! Trust no one! Keep profiler!

tsubakit1.hateblo.jp

tsubakit1.hateblo.jp

【Unity】CinemachineのClear Shotという機能

自分はCinemachineの機能は結構好きで、色々と遊んでみたりしているのです。それでもなお首を撚る系の機能「Clear Shot」についてです。

Clear Shot

Clear ShotはCinemachineに含まれるカメラ挙動の一つで、主な用途は 「キャラクターがカメラ外に行くのを防ぐ」 というものです。他のFollowLookAt系のようなカメラを追随するのではなくアクティブなVirtual Cameraそれ自体を切り替えるという点が特徴といえます。

主な切り替えるための判定は2つです

  • カメラにキャラクターが一定以上近づいた
  • コライダーによってキャラクターが隠れた

キャラクターが隠れた時に、見える位置にカメラを動かす

まずキャラクターが隠れた場合です。

f:id:tsubaki_t1:20181115204335g:plain

例えば下の図では、通常時は青の位置にカメラがありますが、そのままだとオレンジの壁に遮られキャラクター(カプセル君)が見えなくなります。コレを回避するため、高めの位置にVirtual Cameraを配置しておき、それと切り替えることでキャラクターが完全に壁の裏に隠れるのを防ぎます。

f:id:tsubaki_t1:20181115204537j:plain

これは通常のCinemachineColliderと比較して、カメラの切替え先が明確に異なる設定を持つことが可能な点が良いです。 例えば通常は神視点カメラですが、洞窟に入っている間はサードパーソン的な視点とする…といった事も可能です。

f:id:tsubaki_t1:20181115210725g:plain

手順

  1. Cinemachine > ClearShotを選択
  2. CM ClearShot1.CinemachineClearShotLookAtに追跡する対象のGameObjectをセット
  3. CM vcam1を切り替えたいカメラの数だけ増やす。
    その際、常にメインとなるカメラのプライオリティは高めに設定しておく

重要なことは、親(CM ClearShot1)もしくは子の全て(CM vcam1~)のどちらかにCinemachineColliderが設定されていることです。この設定を利用して遮蔽の判定を行っています。遮蔽の判定を行う必要がない場合は、CinemachineColliderAvoid Obstanclesのチェックを外します

f:id:tsubaki_t1:20181115210903j:plain

f:id:tsubaki_t1:20181115211150j:plain

f:id:tsubaki_t1:20181115211200j:plain

なおCinemachineColliderが障害物を検知してめり込みを回避するため、カメラの切替えがCut以外だとカメラが急激に動きます。コレを回避したい場合はCinemachineColliderDumpingWhenOccludedを少し高めに設定しておきます。

f:id:tsubaki_t1:20181115211407j:plain

f:id:tsubaki_t1:20181115211713g:plain

キャラクターが近づいてきた時にカメラを切り替える

次にキャラクターが近づいた場合です。

f:id:tsubaki_t1:20181115212807g:plain

キャラクターがカメラに対して一定以上近くに寄ってきた際に、優先度が切り替わる設定です。実際には遮蔽物で遮られていない、かつ一定範囲内のカメラという条件で取得する際に使う設定のように思います。 下の図でいうところの赤い枠に入るとカメラの切替えが始まります。

f:id:tsubaki_t1:20181115214027j:plain

手順

Optional Target Distanceを設定します。遮蔽物で遮られる予定が無ければAvoid Obstaclesのチェックは外してもOKです。遮蔽物があり複数のカメラが切り替わる場合は、ONにしておきます。

f:id:tsubaki_t1:20181115214327j:plain

使いすぎに注意なのか?

今回紹介したClear Shotですが、使い所はかなり限定されると踏んでいます。

例えば「遮蔽されたときにカメラが切り替わる」場合。今回はカメラの位置を少し上にしただけで、プレイに大した影響は出ないでしょうが、これがカメラの位置がイマジナリーラインを超えてガッツリ変わる場合、ゲームプレイが凄い混乱する気がします。 バイオハザード2やディノクライシスのように、操作性も含めてソレを許容するならば兎も角、こういったカメラ切り替えは最近は余り見ない印象です。いや、特定の位置に近づいたら…等はありそうですが、遮蔽されたら(範囲外に出たら)切り替えるタイプは少なくともパッと思いつきません。

また「近づいたらカメラが切り替わる」、コレはRPGやサードパーソン3Dアクション等でありそうですが、この「近づくと」というのは結構曲者で、カメラの中心点が実際のカメラの位置と異なる場所に調整出来ないと使い勝手が滅法悪いです。 これならばTriggerColliderを用意し、OnTriggerEnter等で反応したカメラの切替えとする方が理にかなっています。また、様々な条件でガチャガチャカメラが切り替わるのは、非常によろしくありません。条件は明確にしておきたい所です。

そして最後に、酔います。今回の記事を書いててカメラをガチャガチャ入れ替えてたんですが、きもちわるい・・・ううっぷ

カメラの操作は、出来るだけプレイヤーが予測できる動きに抑えたい所です。まぁカットシーンやフィルム的なアプローチに利用するのであれば、割とありな気もしますが、その場合はカメラの動きも含めて作っちゃいそうな予感も。

関連

Cinemachine Clear Shotの各項目の設定が日本語で記述されています。

gametukurikata.com

公式チュートリアル動画です。

www.youtube.com

【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