テラシュールブログ

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

【Unit】NativeListをIJobParallelForで使いたい

今回はNativeListIJobParallelForを使用して利用してみます。

NativeListをIJobParallelForで使う

NativeListは要素を増減出来るという点で言えば便利な機能です。ただしIJobを使用している場合には大体の機能を使えるのですがIJobParallelForで書込を行おうとすると、InvalidOperationException: The previously scheduled job Job名 reads from the NativeArray NativeList名. You must call JobHandle.Complete() のようなエラーが表示されて実行できません。

これを回避するには、NativeList.AsDeferredJobArray()を使用して、NativeListをNativeArrayとして扱います。これはコピーではないので、NativeArrayの変更はNativeListにも反映されます。

なお[ReadOnly]を使用している場合には、こんな事をせずともListのまま使用できます。また、NativeArrayに変換しているためAddは出来ません。
また、Jobのパッケージが必要です。

f:id:tsubaki_t1:20190224221935j:plain
JobパッケージのIJobParallelForDeferExtensionsが必要です

使い方(NativeListの用意)

まずNativeListの用意をします。NativeListのAddは並列処理出来ないので、そこは諦めます。

ジョブ側

struct AddJob : IJob
{
    public NativeQueue<int> queue;
    public NativeList<int> list;

    public void Execute()
    {
        while (queue.TryDequeue(out int item))
        {
            if (item % 2 == 0) // 2で割り切れる要素だけ追加
                list.Add(item);
        }
    }
}

呼び出す側

 // Listに登録する要素を
NativeQueue<int> queue = new NativeQueue<int>(Allocator.TempJob);
queue.Enqueue(3); queue.Enqueue(2);
queue.Enqueue(4); queue.Enqueue(5);

NativeList<int> list = new NativeList<int>(8, Allocator.TempJob);

// 内容の更新
var handle = new AddJob { queue = queue, list = list }.Schedule();

使い方(NativeListの内容を更新)

次にNativeListIJobParallelForで更新します。更新する際には上に書いたとおりAsDeferredJobArrayを使用します。特に指定せずとも自動で変換されるっぽいですが、精神の安全のために使用しておきます。

まずジョブですが、普通にNativeArrayを使用します。ここにNativeListの要素が格納されます。

struct UpdateJob : IJobParallelFor
{
    public NativeArray<int> array;

    public void Execute(int index)
    {
        array[index] += 1;
    }
}

使用する側は少しだけ特殊です。まずNativeListをNativeArrayに変換しているのが一点、そしてジョブの長さにNativeListを設定する点です。

後者は普通にNativeListの長さがジョブ発行時にはわからないので、そういった場合の対策といった感じでしょうか。

    handle = new UpdateJob {
        array = list.AsDeferredJobArray()   // AsDeferredJobArrayでNativeArrayにする
    }.Schedule(list, 4, handle);            // 要素数の部分にはlistを登録

これでListの中身を並列処理で書き換えました。

中身を確認する

最後に中身を確認します。こちらは普通に[Readonly]な NativeListで問題ありません。

struct ShowLogJob : IJobParallelFor
{
    [ReadOnly] public NativeList<int> list;

    public void Execute(int index)
    {
        Debug.Log(list[index]);
    }
}

使う側も概ね同じです。

handle = new ShowLogJob
{
    list = list
}.Schedule(list, 8, handle);

実行結果

処理の内容はコードは、

  1. 3/2/4/5をQueueに追加
  2. 2で割り切れる数(2/4)をListに追加
  3. Listの要素に1を追加(2/4 が 3/5になる)
  4. Listの中身をDebugLogで表示

という内容で、実際確認してみると、ちゃんと期待通りの数字が出ています。

f:id:tsubaki_t1:20190224223223j:plain

コード全文(を少し改造したもの)

gist.github.com

感想

NativeListで並列処理出来ないかと色々と確認した結果、最終的にコレになりました。Addが出来ないのは残念ですが、まぁ。

関連

tsubakit1.hateblo.jp

【Unity】ECSでComponentGroup内のComponentDataを取得する新API、ToComponentDataArrayとCopyFromComponentDataArray

ComponentDataArray(型)が非推奨になり、代わりにToComponentDataArray(...)CopyFromComponentDataArray()が追加されました。

これは今までのComponentDataArrayと異なり、NativeArrayを取得します。

ToComponentDataArrayとCopyFromComponentDataArrayはChunkIterationの面倒な記述をスッキリ書ける

このAPIでは、今までComponentDataArrayで取得していたような、ComponentGroup全体が要求するComponentDataの配列を取得できます。

機能は2つで、ComponentGroup.ToComponentDataArray()でComponentGroupが持つComponentData一覧をNativeArrayにコピーして使用できるようようします。また、ComponentGroup.CopyFromComponentDataArray()で変更を反映します。

public NativeArray<T> ToComponentDataArray<T>(Allocator allocator, out JobHandle jobhandle) where T : struct,IComponentData {...}

public void CopyFromComponentDataArray<T>(NativeArray<T> componentDataArray, out JobHandle jobhandle) where T : struct,IComponentData {...}

内部的にはGatherComponentDataJob<T> : IJobChunkで全Chunkが入るバッファを1回で作り、中身を一気に埋めていく感じです。チャンクがそれ程多くないなら、殆ど一回のMemcopyで終わるので効率的に見えます。ChunkIterationでやるような面倒くさい記述を割とスッキリとしてくれるので、割とありがたいです。

ForEachで面倒くさい(例えば複数のGroupを参照するような)ケースでは、こちらが便利そうです。

ということで、使い方を見ていきます。

まずは普通にComponentDataのNativeArrayを取得→反映までをメインスレッドのみで

普通にComponentGroupの要求を定義します。

        // コンポーネントの準備
        playerGroup = GetComponentGroup(
            ComponentType.ReadOnly<Position>(),
            ComponentType.ReadOnly<Player>(),
            ComponentType.Create<HitPoint>());

次にComponentDataを取得します。

ComponentGroup.ToComponentDataArray<T>(Allocator.TempJob);を使用します。

この時Allocatorを設定するのですが、内部的にコピー作業にジョブを使用しているので、実質TempJobもしくはPersistentの二択です。(もしくはTempJob 1択?)

なお内部的にジョブ(&Burst)を使用していますが、即座にCompleteしているので普通にメインスレッド(ComponentSystem)で使って問題ありません。

    protected override void OnUpdate()
    {
        // チャンクからNativeArrayを取得
        var playerPositions = playerGroup.ToComponentDataArray<Position>(Allocator.TempJob);
        var playerHitpoint = playerGroup.ToComponentDataArray<HitPoint>(Allocator.TempJob);
        var enemyPositions = enemyGroup.ToComponentDataArray<Position>(Allocator.TempJob);

これで取得したComponentDataのArrayを使用して、色々と演算出来ます。NativeArrayなのでジョブに投げる事も出来ます。

ただし演算に使用するデータはあくまでコピーされたデータなので、もし変更していた場合は設定を反映させる必要があります。

演算結果をComponentGroupに書き戻します。

下記戻す時にはComponentGroup.CopyFromComponentDataArray(...)を使用します。

        // ダメージを反映
        playerGroup.CopyFromComponentDataArray(playerHitpoint);

ジョブ対応する

ToComponentDataArrayとCopyFromComponentDataArrayは内部的にはIJobChunkのジョブを使用しています。この時JobHandleを渡してやれば、CompleteのタイミングをoutしてきたJobHandleにまかせてくれます。

少し厄介だったのが、JobHandleを受け渡すのが今までのIJob系と若干異なっていた点です。JobではSchedule時に渡しますが、今回はComponentGroup.AddDependency経由で渡します。

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        // チャンクからNativeArrayを取得
        playerGroup.AddDependency(inputDeps);
        var playerPositions = playerGroup.ToComponentDataArray<Position>(Allocator.TempJob, out JobHandle handle1);
        var playerHitpoint = playerGroup.ToComponentDataArray<HitPoint>(Allocator.TempJob, out JobHandle handle2);
        var enemyPositions = enemyGroup.ToComponentDataArray<Position>(Allocator.TempJob, out JobHandle handle3);
        jobhandle = JobHandle.CombineDependencies(handle1, handle2, handle3);

値の反映もジョブ経由です。こちらもAddDependency経由で渡します。正直なんでやねん感はありますが、何故かそうなっています。

        // ダメージを反映
        playerGroup.AddDependency(jobhandle);
        playerGroup.CopyFromComponentDataArray(playerHitpoint, out JobHandle handle4);

ジョブを使わない方の実装例

gist.github.com

ジョブを使う方の実装例

gist.github.com

f:id:tsubaki_t1:20190222005710j:plain
システム内にWaitForJobGroupIDは無くなった

ハイブリットで使いたい場合

ToComponentDataArrayGetComponentDataArray(旧)と異なりコンポーネント(e.g. Transform)を取得出来ません。

そういうのはGetComponentArrayなりGetGameObjectArrayなり、ForEachでやります。(どうせメインスレッドでしか動きませんし)

感想

またAPIが紛らわしいんだよオラァ!!

低レイヤーに直接アクセスを要求するようなAPIが減ってくれて嬉しい限り

関連

tsubakit1.hateblo.jp

tsubakit1.hateblo.jp

【Unity】ECSの「GetSingleton<T>」と「SetSingleton<T>」について

ECSで新しく追加されたAPI GetSingletonSetSingletonというAPIについてです。

一つだけのComponentDataを取得する機能

GetSingleton<T>()SetSingleton<T>(...)ですが、ComponentDataを一つだけ取得して操作する機能です。 

その機能は非常に単純で、同一World内の特定ComponentDataを一つ取得したり設定したりします。Singletonと名前が付いているのは、そのインスタンスが一つだけという前提の機能だからです。*1

Unity的に最もイメージが近いのはGameObject.FindObjectOfType<T>()でしょうか。これも複数の該当オブジェクトがあった場合でも、単一のComponentしか取得しません。

普通のComponentSystemのフィールドと事なり全てのComponentSystemから参照出来る点、そしてstaticと異なりWorld単位で分離している点が特徴として挙げられます。また参照するデータはEntityのComponentDataなので、JobSystemからそう探しやすいという点も少し便利かもしれません。

悪意のある非常識なややこしさですが、デザインパターンであるSingletonのようなクラスのインスタンスが1つしか生成されないことを保証するデザインパターンではないです。 要素が一つ以上あった時に警告が出るようになってました。違う、そうじゃない

実装例

gist.github.com

流れは非常に単純で、GetSingleton<Score>() でWorld内に存在する最初のScoreComponentDataを取得し、SetSingleton<Score>(...)で値を反映させます。

f:id:tsubaki_t1:20190219235650j:plain

なおSingletonと言っても「インスタンスが必ずしもある訳ではない」ので、Entityに何も登録されていなければエラーとなります。例えば今回の場合、ScoreComponentDataが無ければ以下のようなエラーが表示されます。

「InvalidOperationException: GetSingleton<Score>() requires that exactly one Score exists but there are 0.」

これの対策として、初回起動時にComponentDataの有無をチェックします。RequireSingletonForUpdate<T>()を実行時に起動しておくと、ComponentSystemの要求するGroup内にScoreが登録されます。もし該当のComponentSystemがScoreしか要求してないようなら、コレでComponentSysteは処理をスキップするようになります。

ジョブの終了待ちを行う

GetSingletonが扱うデータはComponentDataのため、JobSystem上で処理することが出来ます。ただし当然他のジョブが操作しているときにGetSingletonを取得する等は出来ません。つまり、GetSingletonは該当のComponentDataを処理するジョブが完了するまで待機します。

これは場合によってはメインスレッドを結構長い時間止めてしまうことになるかもしれません。

感想

殆どのケースで、ComponentSystemのフィールドに値を登録するほうが理に適ってる気がするのですが、どうなんでしょう。

*1:特に制限を設けていないので、複数作ることは可能

【Unity】Terrainで木の影をベイクする

f:id:tsubaki_t1:20190219011332j:plain

今回はTerrainで「木の影」をベイクする方法についてです。

木の影をベイクしたい

影はほとんどの場合、描画範囲を広くすれば広くするほど汚くなります。これは逆に描画範囲を狭くすれば狭くするほどに、1ピクセル辺りに使える影のサイズが増えるので綺麗に出来ます。とは言え、影を近距離しか描画しないと、遠距離の影の表現が無くなってしまい、ソレはソレで残念な表現になってしまいます。

f:id:tsubaki_t1:20190219012200j:plain
表示距離120mまで

f:id:tsubaki_t1:20190219012218j:plain
表示距離20mまで

ということでShadow MaskDistance Shadowを使用して、20mの範囲はリアルタイムな影を、20mより先は低解像度なShadowMaskを使用するというアプローチを考えてみます。これで、比較的広いマップでも割りかし綺麗な表現が期待出来ます。

f:id:tsubaki_t1:20190219012332j:plain
Distance Shadow

木の影がベイク出来ない

さて、Shadow MaskをTerrainの木に適応したい所ですが、上手く影が焼けません。地面は普通に影を焼けるのですが、木から落ちる影を焼くことは出来ないといった状態です。これは特に多くの木を置いたりすると、非常に目立ちます。

f:id:tsubaki_t1:20190219013016j:plain
Cubeは影を焼けたのでカメラを離しても表示されるが、木は表示されない

f:id:tsubaki_t1:20190219013543j:plain
奥の森に影が無い

対策

Terrainの木を生成するとき、SpeedTreeで生成したアセットを直接指定すると、上のような結果になります。これはLightmapのstaticが入っていないからです。なので、木のオブジェクトにLightmapのstaticを設定してやります。

  1. SpeedTreeのアセットを選択(TerrainのTree>木を選択>Editが手っ取り早いです)
  2. SpeedTreeのアセットをシーンに配置→Projectビューに配置でPrefab化
  3. PrefabのLightmap staticを有効にする。特にRootオブジェクトは必ずLightmap staticを有効にする
  4. TerrainのTreeのEditで、Terrainが表示する木をPrefabに差し替える
  5. ライトマップを焼く

f:id:tsubaki_t1:20190219015006g:plain
影をベイクする手順

これで木の影がベイクされます。影が表示されない場合は、ライトマップの解像度が低いのかもしれません。設定を少し上げておきます。

f:id:tsubaki_t1:20190219015053j:plain
ちゃんと範囲外の影も表示される

f:id:tsubaki_t1:20190219015417j:plain
近づくと綺麗な影が確認出来る

感想

Terrain試してた時に「そういえばコメントで名無しさんが補足してくれたな」と思い出して、やってみました。 コメント感謝です。

関連

tsubakit1.hateblo.jp