simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

CubeWalkGame非同期読み込みと近傍優先処理

CubeWalk シリーズです。
1.Unity ECS による動的頂点生成と面生成
2.チャンクをまたぐキューブデータ参照
3.キューブの回転表現とテクスチャの貼り付け
4.チャンクデータの永続化と描画負荷低減のための階層化
5.プレイヤーカメラが移動するタイミングでメッシュをアップデート
と進めてきました。

前回はこちら
simplestar-tech.hatenablog.com

今回は同期で一度に読み込んでいたチャンクデータを非同期処理で読み込み、ロード完了のイベントでメッシュ作成のキューイングを行うようにします。
プレイヤーチャンクの移動がロード中に走った場合は、ロードをキャンセルしてチャンクデータの読み込みをプレイヤーを中心に割り込ませます。

ワールドの更新のための機能はこれが最後になる予定です。
期待通り動く絵を作るため、具体的な実装を考えていきましょう。

f:id:simplestar_tech:20190701224336j:plain
実装した結果、無限に続く世界の非同期ロードによる更新が確認できました

# 同期処理を非同期処理へ

ファイル読み込み部分を確認すると…

        if (File.Exists(filePath))
        {
            UnsafeFileUtility.ReadData(filePath, out var fileData);
            pChunkData = (int*)fileData.Buffer;
        }

この関数内は?

    /// <summary>
    /// 呼び出し元は fileData.Buffer に対し ReleaseReadData で開放する責任あり
    /// </summary>
    public static void ReadData(string filePath, out ReadCommand fileData)
    {
        var fileInfo = new System.IO.FileInfo(filePath);
        long fileSize = fileInfo.Length;

        var readCommand = new NativeArray<ReadCommand>(1, Allocator.Persistent);
        readCommand[0] = new ReadCommand
        {
            Offset = 0,
            Size = fileSize,
            Buffer = UnsafeUtility.Malloc(fileSize, UnsafeUtility.AlignOf<byte>(), Allocator.Persistent),
        };

        var readHandle = AsyncReadManager.Read(filePath, (ReadCommand*)readCommand.GetUnsafePtr(), 1);
        readHandle.JobHandle.Complete();
        fileData = readCommand[0];
        
        readHandle.Dispose();
        readCommand.Dispose();
    }

もともと非同期処理を、handle の Complete でブロックして完了を待っています。
ここを 非同期関数にしてみます。

    /// <summary>
    /// 呼び出し元は fileData に対し ReleaseReadData で開放する責任あり
    /// さらに readHandle.IsValid() && readHandle.Status != ReadStatus.InProgress になるまで監視して readHandle.Dispose() で開放する責任あり
    /// </summary>
    public static void ReadData(string filePath, out ReadHandle readHandle , out ReadCommand fileData)
    {
        var fileInfo = new System.IO.FileInfo(filePath);
        long fileSize = fileInfo.Length;

        var readCommand = new NativeArray<ReadCommand>(1, Allocator.Persistent);
        readCommand[0] = new ReadCommand
        {
            Offset = 0,
            Size = fileSize,
            Buffer = UnsafeUtility.Malloc(fileSize, UnsafeUtility.AlignOf<byte>(), Allocator.Persistent),
        };

        readHandle = AsyncReadManager.Read(filePath, (ReadCommand*)readCommand.GetUnsafePtr(), 1);
        fileData = readCommand[0];
        
        readCommand.Dispose();
    }

コメントの通り、呼び出し元で現在のステータスをチェックして、プロセスが完了していたら正しくデータが格納されていることを確認できました。

で、これを今の呼び出しだと、次の通り
3x3x3 の 27 チャンクのさらに外周についてもロードを for 文で回し 5 x 5 x 5 の 125 チャンクデータのロードを走らせ
成功したときに、コアのチャンクのインデックス情報と共に、メッシュ作成とチャンク結合のイベントを投げるようにしています。

    /// <summary>
    /// チャンクデータのロード
    /// </summary>
    internal void DownloadWorld(Vector3Int centerChunkInt3)
    {
        // 近景チャンク半径1 + 1(無用メッシュ境界を作らないため)でチャンクデータをロード
        var loadChunkRadius = nearMergeRadius + 1;
        for (var x = -loadChunkRadius; x <= loadChunkRadius; x++)
        {
            for (var z = -loadChunkRadius; z <= loadChunkRadius; z++)
            {
                for (var y = -loadChunkRadius; y <= loadChunkRadius; y++)
                {
                    LoadChunkData(new Vector3Int(centerChunkInt3.x + x, centerChunkInt3.y + y, centerChunkInt3.z + z));
                }
            }
        }
        this.onDownloadChunkDataEvent?.Invoke(centerChunkInt3, -1, centerChunkInt3);
        // 遠景チャンクを 1~ max 半径まで周回しながらチャンクデータをロード
        int combineCoreChunkIndex = 0;
        for (var coreChunkLevel = 1; coreChunkLevel < farChunkRadius; coreChunkLevel++)
        {
            var offset = nearMergeRadius * 2 + 1;
            var geta = coreChunkLevel * offset;
            for (var x = -geta; x <= geta; x += offset)
            {
                for (var z = -geta; z <= geta; z += offset)
                {
                    for (var y = -geta; y <= geta; y += offset)
                    {
                        if (0 != geta - Mathf.Abs(x) && 0 != geta - Mathf.Abs(y) && 0 != geta - Mathf.Abs(z))
                        {
                            continue;
                        }
                        for (var radiusX = -loadChunkRadius; radiusX <= loadChunkRadius; radiusX++)
                        {
                            for (var radiusZ = -loadChunkRadius; radiusZ <= loadChunkRadius; radiusZ++)
                            {
                                for (var radiusY = -loadChunkRadius; radiusY <= loadChunkRadius; radiusY++)
                                {
                                    if (0 == radiusX && 0 == radiusY && 0 == radiusZ)
                                    {
                                        continue;
                                    }
                                    LoadChunkData(new Vector3Int(centerChunkInt3.x + x + radiusX, centerChunkInt3.y + y + radiusY, centerChunkInt3.z + z + radiusZ));
                                }
                            }
                        }
                        var coreChunkInt3 = new Vector3Int(centerChunkInt3.x + x, centerChunkInt3.y + y, centerChunkInt3.z + z);
                        LoadChunkData(coreChunkInt3);
                        this.onDownloadChunkDataEvent?.Invoke(coreChunkInt3, combineCoreChunkIndex++, centerChunkInt3);
                    }
                }
            }
        }
    }

現在はそのイベントの先にて、以下の通りチャンクメッシュ作成の順序を守りながら Enqueue していますが…

    void OnLoadCoreChunkData(Vector3Int coreChunkInt3, int mergeCoreChunkIndex, Vector3Int centerChunkInt3)
    {
        // コア周辺のチャンクのメッシュ作成情報を Enqueue
        var meshChunkRadius = nearMergeRadius;
        for (var radiusX = -meshChunkRadius; radiusX <= meshChunkRadius; radiusX++)
        {
            for (var radiusZ = -meshChunkRadius; radiusZ <= meshChunkRadius; radiusZ++)
            {
                for (var radiusY = -meshChunkRadius; radiusY <= meshChunkRadius; radiusY++)
                {
                    if (0 == radiusX && 0 == radiusY && 0 == radiusZ)
                    {
                        continue;
                    }
                    this.gameLogic.createEntityQueue.Enqueue(new CreateChunkInfo {
                        chunkInt3 = new Vector3Int(coreChunkInt3.x + radiusX, coreChunkInt3.y + radiusY, coreChunkInt3.z + radiusZ),
                        mergeCoreChunkIndex = -1,
                        centerChunkInt3 = centerChunkInt3
                    });
                }
            }
        }
        // 最後にコアのチャンクメッシュ作成を Enqueue
        var createChunkInfo = new CreateChunkInfo {
            chunkInt3 = coreChunkInt3,
            mergeCoreChunkIndex = mergeCoreChunkIndex,
            centerChunkInt3 = centerChunkInt3
        };
        this.gameLogic.createEntityQueue.Enqueue(createChunkInfo);
    }

愚直に、ダウンロードの順序を守りながら非同期処理を行うとして、ダウンロードもキューイングして
非同期処理完了後に、ダウンロード情報をキューからデキューして再度非同期処理を走らせると良いと思います。

非同期によるフレーム分散の処理の先で、さらにフレーム分散のためのキューイングを行う感じですね。
キューに詰める情報として、コアとしてのメッシュ作成のためのキューイングのための情報も詰める形です。

キューイングの先でキューイングという…
ファイルロード、メッシュ結合、2つの重たい処理を負荷分散する形です。

これが完成したら、2つのキューをクリアする機能を用意して
これをプレイヤーチャンク更新のタイミングで走らせてから、再度キューに詰め直すという流れをイメージして完璧な動きを想像できています。

さぁ、あとは自分がどれだけ正しい未来を予見しているかの証明ですね。
作ってみて結果を報告します。

実装した結果は次の通り

実装中に困ったこと
1.チャンクデータがファイルとして無い場合は作っているが、その関数を非同期化したい
処理が読み込みと作成で分岐するけど、それを非同期で動かそうとしているので、可能なら作成も非同期化したいが…

タスクの情報はキューには詰めているし
非同期の Task は使えるし、コールバックは走らせられる
メインスレッドでコールバックもできる

ということは、キューに詰める情報には開放必要なものは置かず
メインスレッドは非同期処理の内容を気にせず好きなタイミングでキューをクリアしたり情報を詰めたりできる
非同期処理がキックされたら、引数情報をもとに非同期処理をして、完了をメインスレッドでコールバック
コールバック内で、キューからデキューした引数情報を再帰的に非同期処理に乗せる

ん、非同期処理のキックが大量に行われると困ったことに…
一つの非同期処理が走っている間は、多重キックしないようにしないといけない

Task 実行中ってわかるには?
docs.microsoft.com

タスクの最終状態としては、RanToCompletion、Canceled、Faultedのいずれかになります。利用可能性の高いこれらの値をより便利に利用できるよう、TaskクラスにIsCompleted、IsCanceled、IsFaultedプロパティが提供されています。IsCompletedはStatusプロパティがRanToCompletion、Canceled、Faultedのいずれかのときにtrueを返すので注意が必要です。

となると、次のコードで目的の動作が実現できるか

    Task LoacChunkDataAsync(ChunkLoadInfo chunkLoadTask)
    {
        return Task.Run(() => {
            // ここでロード or 作成
        });
    }

タスク内でメインスレッド Invoke するには?
こうしました。(動作確認済み)

        var mainContext = SynchronizationContext.Current;
        return Task.Run(() => {
            // ここでロード or 作成
            LoadChunkData(chunkLoadTask.coreChunkInt3, chunkIndex, chunkKeyXYZ, filePath);
            if (0 != chunkLoadTask.combineCoreChunkIndex)
            {
                mainContext.Post(_ => {
                    this.onDownloadChunkDataEvent?.Invoke(chunkLoadTask.coreChunkInt3, chunkLoadTask.combineCoreChunkIndex, chunkLoadTask.centerChunkInt3);
                }, null);
            }
        });

一応上記の対応で負荷が上がることなく、非同期読み込みの結果のチャンク生成までできました…が
一番小さい 3x3x3 の結合の外周半径1で、全メッシュ作成に 60000 ms かかってしまい、遅すぎると…
目標としては 10000 ms 以内としたいので、あと 6倍以上は高速化したいところ

少し実装内容を変えていきます。
具体的には、非同期処理内でスレッドセーフキューをループして取り出す仕組みです。

すると 3000 ms まで高速化しました。20倍の高速化です。
軽く動作確認してみます。

プレイヤーが高速移動して、既存の処理を飛び越えるようなことすると null 参照エラーで
ppChunk のポインタが無効と System 側から怒られる

原因の把握と対策をしてみます

途中から非同期処理のために Task 内で Random.Range 使うことになってた…

これがまずかった模様

次の通り書き換えることで解決しました。

            var r = new Random(seed: (uint)System.DateTime.Now.Ticks);
                        pData[1] = (byte)r.NextInt((int)CubeRotationType.Top000, (int)CubeRotationType.Max);

あとは、プレイヤーチャンクの更新でキューに積んだ処理を律儀にこなし続けるので、プレイヤーチャンクを更新したらスレッドセーフなキューをクリアするようにします。

できました。


続きはこちら
simplestar-tech.hatenablog.com

動くサンプルはここに
github.com

CubeWalkGameプレイヤーカメラ移動によるチャンクの取捨選択

最後まで読むと得られる結果

このシリーズの続きです。
simplestar-tech.hatenablog.com

1.Unity ECS による動的頂点生成と面生成
2.チャンクをまたぐキューブデータ参照
3.キューブの回転表現とテクスチャの貼り付け
4.チャンクデータの永続化と描画負荷低減のための階層化

と階段を登ってきました。

今回は、プレイヤーカメラが移動すると、世界の描画範囲も合わせて移動する表現の実装を行っていきます。
イメージはスッと思い浮かぶのですが、それを具体的な手段に落とし込んで記録します。

# プレイヤーチャンクの特定

今まではとりあえず -12 ~ 12 の範囲のチャンクを対象にシーンにドンと配置する操作でした。
一応、カメラはデバッグ用に使いたいので、擬似的なプレイヤーオブジェクトをシーンに配置して、そのプレイヤーが移動すると
対象のプレイヤーチャンクが決定されるというコードを書いてみます。

シーンにあるプレイヤーオブジェクト
そろそろ何がプレイヤーの動きを監視するのでしょうか?

一度コード整理します。

アプリ全体に関わる ECS まわりの設定などはエントリポイント的な GameLogic に書きました。

そして、新しくクラスを用意し
そこで Update を構えて、その中でプレイヤーカメラの位置を追い続けるようにします。

クラス名は後で変えようと思うけど、プレイヤーカメラの位置によって世界を更新するものだから
そういう名前で

プレイヤーチャンクの決め方は簡単で、カメラ座標にチャンクの半分の長さを足して、チャンク中心までの距離を計算し
最も近い位置にあるチャンクがプレイヤーチャンクです。

走査する必要はなく、Round で丸めた値を int にすればチャンク位置であり
これをキーに変換する関数は前回作りましたので、これでチャンクを一意に特定できます。
どんな位置にいても

ロジックを組むと次の通り(動作確認済み)

using UnityEngine;

internal class ChunkWorld : MonoBehaviour
{
    #region Scene Components
    [SerializeField] Transform playerCamera;
    #endregion

    void Start()
    {
        
    }

    void Update()
    {
        var chunkSideOffset = ChunkConst.CubeSide * new Vector3(ChunkConst.ChunkSizeX, ChunkConst.ChunkSizeY, ChunkConst.ChunkSizeZ) / 2;
        chunkSideOffset = playerCamera.position - chunkSideOffset;
        var chunkInt3 = new Vector3Int(Mathf.RoundToInt(chunkSideOffset.x / ChunkConst.ChunkSizeX),
            Mathf.RoundToInt(chunkSideOffset.y / ChunkConst.ChunkSizeY),
            Mathf.RoundToInt(chunkSideOffset.z / ChunkConst.ChunkSizeZ));

        ChunkInt3ToChunkKey(chunkInt3, out var chunkKeyX, out var chunkKeyY, out var chunkKeyZ);
        Debug.Log($"{chunkKeyX}, {chunkKeyY}, {chunkKeyZ}");
    }

    internal static void ChunkInt3ToChunkKey(Vector3Int chunkInt3Position, out int chunkKeyX, out int chunkKeyY, out int chunkKeyZ)
    {
        var byteMax = (byte.MaxValue + 1);
        chunkKeyX = chunkInt3Position.x % byteMax;
        if (0 > chunkKeyX)
            chunkKeyX = (byte)chunkKeyX;
        chunkKeyY = chunkInt3Position.y % byteMax;
        if (0 > chunkKeyY)
            chunkKeyY = (byte)chunkKeyY;
        chunkKeyZ = chunkInt3Position.z % byteMax;
        if (0 > chunkKeyZ)
            chunkKeyZ = (byte)chunkKeyZ;
    }
}

internal class ChunkConst
{
    public const int ChunkSizeX = 16;
    public const int ChunkSizeY = 16;
    public const int ChunkSizeZ = 16;

    public const float CubeSide = 1f;
}

# プレイヤーチャンク移動タイミングの定義

毎回上記の更新作業をすると計算量がちょっとだけ勿体ないのと、境界でプレイヤーが振動したときに切り替わりが高速に走って
その後のチャンクメッシュの更新スピードに見合わないキューイングが発生しそうです。

一度プレイヤーチャンクのキー情報を特定したら、まずはそのチャンクから一定距離離れるまでは上記の更新作業を行わないようにすることをずっと考えていました。
実現してみます。

距離はチャンクの中心から、チャンクの最小外接球の半径としてみます。
キューブの中心から頂点までの距離は、立方体なら √3/2 (一辺が 1 なら)なので

次の通り(動作確認済み)

    void Start()
    {
        this.minChunkSize = Mathf.Sqrt(3f) / 2 * ChunkConst.CubeSide * Mathf.Min(ChunkConst.ChunkSizeX, ChunkConst.ChunkSizeY, ChunkConst.ChunkSizeZ) + 0.5f;
    }

    void Update()
    {
        this.UpdatePlayerChunk();
    }

    /// <summary>
    /// プレイヤーカメラの位置を使ってプレイヤーチャンクを更新
    /// </summary>
    void UpdatePlayerChunk()
    {
        var distancePlayerToChunk = Vector3.Distance(this.playerCamera.position, this.playerChunkCenter);
        if (this.minChunkSize < distancePlayerToChunk)
        {
            var chunkSideOffset = ChunkConst.CubeSide * new Vector3(ChunkConst.ChunkSizeX, ChunkConst.ChunkSizeY, ChunkConst.ChunkSizeZ) / 2;
            var playerOffsetPosition = this.playerCamera.position - chunkSideOffset;
            var chunkInt3 = new Vector3Int(Mathf.RoundToInt(playerOffsetPosition.x / ChunkConst.ChunkSizeX),
                Mathf.RoundToInt(playerOffsetPosition.y / ChunkConst.ChunkSizeY),
                Mathf.RoundToInt(playerOffsetPosition.z / ChunkConst.ChunkSizeZ));

            ChunkInt3ToChunkKey(chunkInt3, out var chunkKeyX, out var chunkKeyY, out var chunkKeyZ);
            this.playerChunkKey = new Vector3Int(chunkKeyX, chunkKeyY, chunkKeyZ);
            this.playerChunkCenter = new Vector3(chunkInt3.x * ChunkConst.ChunkSizeX, chunkInt3.y * ChunkConst.ChunkSizeY, chunkInt3.z * ChunkConst.ChunkSizeZ) + chunkSideOffset;

            Debug.Log($"{chunkKeyX}, {chunkKeyY}, {chunkKeyZ}");
        }
    }

    float minChunkSize = ChunkConst.ChunkSizeX;
    Vector3Int playerChunkKey = Vector3Int.zero;
    Vector3 playerChunkCenter = ChunkConst.CubeSide * new Vector3(ChunkConst.ChunkSizeX, ChunkConst.ChunkSizeY, ChunkConst.ChunkSizeZ) / 2;

# 既存のチャンクメッシュを利用する再結合処理

プレイヤーチャンクの切り替えタイミングまではっきりしましたが…
メッシュを作成するには周辺すべてのチャンクデータがロード済みでなければならず
さらに、必要なデータが全部揃ってからキューにチャンクメッシュ作成命令を積むべきものです。

近景はロード済みになっているはずなので、メッシュ更新は即時更新されるべきであり
遠景はロード完了を確認してからメッシュ更新を走らせるのが良さそうですが

実装コードになりますかね…

遠景はチャンクを複数結合しているため、その結合を解いて、作り直すという絵が見えます。

プレイヤーチャンクのキーから、次の情報が即時参照できますが、どうなることやら

    int** ppChunkData = null;                   // ワールドのチャンクデータ
    MeshFilter[] worldChunkMeshFilters = null;  // ワールドのチャンク MeshFilter

対象のチャンクオブジェクトのメッシュから、マージメッシュが取れたり
マージメッシュから、マージしているメッシュ一覧が効率的に取れると良さそう

事前の懸念はこれくらいにして、愚直に実装して壁にあたってみたいと思います。

プレイヤーチャンクの更新をイベントにして、そのイベントからコレまで通りのメッシュ更新を走らせてみましょう。

確認できた問題は2つ
1.現在のチャンクメッシュの更新は中央のチャンクが 000 固定…イベントの引数で切り替わるように作らなければならない
2.前回ロードしたチャンク情報を見ていないので、無駄な読み込みが走る

この問題は難しいので
・遅くても機能して無駄のない動きをすること
・原理的に絶対にフレームレートが落ちないこと
この2つに分けて、先に機能として十分で無駄なことをしない処理を目指します。

1.については機能の漏れなので、対応は想像しながらコーディングでいけそう
問題なく
こんな感じでいけました。

    /// <summary>
    /// ダウンロード完了イベントとキュー詰めは分けるべき @todo
    /// </summary>
    internal void DownloadWorld(Vector3Int chunkInt3)
    {
        var perChunkRadius = 1;
        for (var x = -perChunkRadius; x <= perChunkRadius; x++)
        {
            for (var z = -perChunkRadius; z <= perChunkRadius; z++)
            {
                for (var y = -perChunkRadius; y <= perChunkRadius; y++)
                {
                    this.EnqueueCreateEntity(chunkInt3.x + x, chunkInt3.y + y, chunkInt3.z + z, 0);
                }
            }
        }

2.は参照すべき情報があるのだから、それを見てロード済みだったらスキップでいけるはず

ファイル読み込みはこれでスキップできた

    void EnqueueCreateEntity(int x, int y, int z, int margeRadius)
    {
        int chunkKeyX, chunkKeyY, chunkKeyZ;
        ChunkWorld.ChunkInt3ToChunkKey(new Vector3Int(x, y, z), out chunkKeyX, out chunkKeyY, out chunkKeyZ);
        var byteMax = (byte.MaxValue + 1);
        int chunkIndex = chunkKeyX * byteMax * byteMax + chunkKeyZ * byteMax + chunkKeyY;

        // 未読込の時にロード or 作成
        if (null == this.ppChunkData[chunkIndex])
        {
            LoadOrCreateChunkData(new Vector3Int(chunkKeyX, chunkKeyY, chunkKeyZ), new Vector3Int(x, y, z), out var pChunkData);
            this.ppChunkData[chunkIndex] = pChunkData;
        }

まだ、メッシュの無駄作成が走っているので、これもスキップする
動的にチャンクデータが更新されることを今後やったときに不具合になるけど、今回はチャンクデータ固定なので無視する、そんときは更新フラグを追加して更新されてたらメッシュを更新するとかにしよう

ということでメッシュ作成もスキップするコードはこちら
System からも world のチャンクデータを参照する必要がでてきたので static internal でプロジェクト内のどこからでも編集、参照できるようにしてしまった…

チャンクのメッシュ作成もすでにメッシュが作られている場合は処理をスキップするようにして、無駄なメッシュ作成は行われなくなった。
コードはこんな感じに

    protected unsafe override void OnUpdate()
    {
        #region NativeArray 確保
        var entities = this.query.ToEntityArray(Allocator.TempJob);
        var meshDataArray = this.query.ToComponentDataArray<ChunkMeshData>(Allocator.TempJob);
        #endregion

        #region エンティティごとにすでにメッシュが作成済の場合はスキップフラグを設定
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var meshData = meshDataArray[entityIndex];
            var byteMax = (byte.MaxValue + 1);
            int chunkIndex = meshData.chunkKeyX * byteMax * byteMax + meshData.chunkKeyZ * byteMax + meshData.chunkKeyY;
            meshData.skipFlag = null != ChunkWorld.worldChunkMeshFilters[chunkIndex] ? (byte)1 : (byte)0;
            meshDataArray[entityIndex] = meshData;
        }
        #endregion

        #region メッシュの頂点数をカウント
        var countVerticesJob = new CountVerticesJob
        {
            ppRotationFaceDirection = this.ppRotationFaceDirection,
            sourceCount = this.nativeVerticesSource.Length,
            meshDataArray = meshDataArray
        };
        var countJobHandle = countVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        countJobHandle.Complete();
        #endregion

        #region カウント数→頂点バッファを確保→バッファポインタを ComponentData に代入
        var entityMeshDataArray = new EntityMeshData[entities.Length];
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var meshData = meshDataArray[entityIndex];
            if (1 == meshData.skipFlag)
            {
                continue;
            }

結果から不思議に思ったことが…

f:id:simplestar_tech:20190623165746p:plain
拡張された領域はチャンク結合されない、なんでこうなる?

少し考えて、すぐにひらめきました。
核のイベントをスキップしている

核チャンクでキューイングされている場合はイベントを発火するようにします。

f:id:simplestar_tech:20190623170945p:plain
問題が一つ解決して、また問題が発覚 前のチャンク結合は残り、無用な側面も複製される

ここで明確な不具合が2つあります。
不具合1.無用な境界メッシュが作られている
不具合2.以前のチャンク結合が残っている

ひらめくアイディアに
不具合1は、データの読み込み領域を1チャンク広くして
データ読み込み完了をもってチャンクメッシュ作成をキューイング

不具合2は、全結合チャンクが作られてからクリア
クリアするときに、今回のキューイングで作成対象から外れているチャンクメッシュを一緒に削除

不具合1の解決方法を、イメージしながら手を動かして解決するか見ていきます。

まず、ダウンロードとキューイングは分けます。
分けた後で、ダウンロードの方の半径を大きく設定します。
あと、ちょうど階層化してループしているので
プレイヤーチャンクを中心とした 27 チャンク + 1 チャンク半径のダウンロード完了と
外周全部 + 1 チャンク半径のダウンロード完了時に異なるイベントを発行します

そのタイミングでチャンクメッシュ作成のハンドラが一つ内側のデータを使ってメッシュをカウントするようにしていきます。
だんだんアプリよりの実装になってきました。

書いてて気づいたことに、階層化した外周についても、ダウンロードも一括で行った後ではなく
外周のチャンク結合のコアの周辺が全てダウンロード完了したらイベントを発火して、キューイングするのが良さそう

そうすればダウンロードが非同期で行われた後も、イベントを頼りにメッシュを作っていけばいい

そうして実装したのがこちら

    /// <summary>
    /// チャンクデータのロード
    /// </summary>
    internal void DownloadWorld(Vector3Int centerChunkInt3)
    {
        var loadChunkRadius = 1 + 1;
        for (var x = -loadChunkRadius; x <= loadChunkRadius; x++)
        {
            for (var z = -loadChunkRadius; z <= loadChunkRadius; z++)
            {
                for (var y = -loadChunkRadius; y <= loadChunkRadius; y++)
                {
                    LoadChunkData(new Vector3Int(centerChunkInt3.x + x, centerChunkInt3.y + y, centerChunkInt3.z + z));
                }
            }
        }
        this.onDownloadChunkDataEvent?.Invoke(centerChunkInt3, 0);

        for (var level = loadChunkRadius; level <= 3; level++)
        {
            if (2 > level)
            {
                break;
            }
            var offset = 3;
            var geta = level * offset - 3;
            for (var x = -geta; x <= geta; x += offset)
            {
                for (var z = -geta; z <= geta; z += offset)
                {
                    for (var y = -geta; y <= geta; y += offset)
                    {
                        if (0 != geta - Mathf.Abs(x) && 0 != geta - Mathf.Abs(y) && 0 != geta - Mathf.Abs(z))
                        {
                            continue;
                        }
                        for (var radiusX = -loadChunkRadius; radiusX <= loadChunkRadius; radiusX++)
                        {
                            for (var radiusZ = -loadChunkRadius; radiusZ <= loadChunkRadius; radiusZ++)
                            {
                                for (var radiusY = -loadChunkRadius; radiusY <= loadChunkRadius; radiusY++)
                                {
                                    if (0 == radiusX && 0 == radiusY && 0 == radiusZ)
                                    {
                                        continue;
                                    }
                                    LoadChunkData(new Vector3Int(centerChunkInt3.x + x + radiusX, centerChunkInt3.y + y + radiusY, centerChunkInt3.z + z + radiusZ));
                                }
                            }
                        }
                        var coreChunkInt3 = new Vector3Int(centerChunkInt3.x + x, centerChunkInt3.y + y, centerChunkInt3.z + z);
                        LoadChunkData(coreChunkInt3);
                        this.onDownloadChunkDataEvent?.Invoke(coreChunkInt3, 1);
                    }
                }
            }
        }
    }

ダウンロード後のイベントハンドラがこちら

    void OnDownloadChunkData(Vector3Int coreChunkInt3, int margeRadius)
    {
        // コア周辺のチャンクのメッシュ作成情報を Enqueue
        var meshChunkRadius = 1;
        for (var radiusX = -meshChunkRadius; radiusX <= meshChunkRadius; radiusX++)
        {
            for (var radiusZ = -meshChunkRadius; radiusZ <= meshChunkRadius; radiusZ++)
            {
                for (var radiusY = -meshChunkRadius; radiusY <= meshChunkRadius; radiusY++)
                {
                    if (0 == radiusX && 0 == radiusY && 0 == radiusZ)
                    {
                        continue;
                    }
                    this.gameLogic.createEntityQueue.Enqueue(new CreateChunkInfo {
                        chunkInt3 = new Vector3Int(coreChunkInt3.x + radiusX, coreChunkInt3.y + radiusY, coreChunkInt3.z + radiusZ), mergeRadius = 0 });
                }
            }
        }
        // 最後にコアのチャンクメッシュ作成を Enqueue
        var createChunkInfo = new CreateChunkInfo { chunkInt3 = coreChunkInt3, mergeRadius = margeRadius };
        this.gameLogic.createEntityQueue.Enqueue(createChunkInfo);
    }

f:id:simplestar_tech:20190623181839p:plain
ダウンロードの半径を増やして、メッシュは不要な面を作らなくなった

期待通り動いている様子

f:id:simplestar_tech:20190623182206p:plain
次はこの、前に作ったチャンク結合メッシュを片付けます

ここでチャンクに参照カウンタを取り入れようと思います。
チャンク結合メッシュオブジェクトの削除のタイミングで、構成要素となっているチャンクオブジェクトの結合メッシュへの参照数を数えて
もし自身の 1 だったらそのまま Destroy するというもの

ここで見落としていた不具合が2つ

1.下図

f:id:simplestar_tech:20190624080113p:plain
プレイヤー周辺のチャンクメッシュのアクティブ化漏れ
移動後は 3x3x3 で周囲 27 チャンクがアクティブでなければならないのに

2.過去の結合チャンクの削除タイミング
先に次の世代の結合チャンクが作られていなければならない
また、近づいたときに残っていてはならないので、残し続けてもいけない
同時に消すというよりかは、次の世代の結合チャンクが作られるタイミングで順次消していってほしい

不具合に遭遇
これまで重複する場所に、メッシュオブジェクトを作成していることに気づきました。
すでに作成済みの gameObject を利用するように処理を書き換えたところ、EntityManager が null になっていて再利用できないことがわかりました。

どうも gameObject 自体が非アクティブだと、EntityManager は null 扱いになるらしいです。
そこで gameObject を非アクティブにするのではなく meshRenderer を非表示にするようにしました。

解決しました。

f:id:simplestar_tech:20190624215423p:plain
メッシュオブジェクトの再利用により、不具合1が解決

ふと思いついた絵に、結合チャンクの数は変わらないので、前回と同じインデックスに相当するチャンク結合は 1 チャンクだけずれて存在しているに違いない
そこで、前回と同じインデックスに属するチャンク結合を解き、そのときに結合したチャンクの描画をアクティブに戻す
ただし、参照カウンタを減らして 0 のものだけ、もし参照カウンタを減らしたのに 0 より大きい場合はすでに別のチャンク結合に所属しているので、描画の必要がない

これなら近くにある結合チャンクから先に削除されるので、描画用の結合チャンクにプレイヤーが混乱することは少なくなりそう
孤立したチャンクのうち、中心のチャンクから最大半径以上離れているものがあれば、これを削除する
で、削除するのはチャンクのメッシュだけではなく、チャンクのデータもなので
そのチャンクの周辺 27 個についても削除を走らせることにする

これで完璧に動きそうなので、実装して試してみます。

実装中困ったこと

結合チャンクコアはイベントで処理しているので、何番目のコアなのかのインデックスがわからない
イベントを発行する側でインデックスを作って渡せないか?

イベント発行元は ComponentSystem なので、順序はエンティティのオブジェクトコンポーネントに入っていると取り出せる
エンティティのマーカーコンポーネント追加場所を追ってみる
チャンクエンティティを作成する関数にたどりついたけど、ここにもそのようなインデックスを決定する処理はない
更に追うと、キューからデキューして作成のための情報を処理している
となると、ここでチャンクのインデックスやマージするコアのインデックスを渡せるか考えてみる
OnDownloadChunkData というイベントハンドラが呼び出し元、ここでもインデックス情報は参照できない
となると、このイベントを発行している元をさがしてみます。
DownloadWorld という関数で for ループを回してダウンロード処理を作っている ここですね。
ここならコアのインデックスを数えることができます。
このイベントの引数から情報を伝播させて、末端のマーカーコンポーネント追加処理まで情報を渡しましょう

実装することで、まず前回と同じインデックスに属するチャンク結合を解き、そのときに結合したチャンクの描画をアクティブに戻す
まで動作確認しました。

アクティブで孤立しているチャンクと遠く離れすぎた周辺データを削除するための情報が抜けているので、これにも対処します。

実行時の中心チャンクの位置int3情報と結合する chunk の半径がわかれば良さそうなので
これも同じようにイベント引数で伝播させてみます。

不要なチャンクの削除まで機能するところまで来ました。
チャンク結合を削除前に構成しているチャンクの表示を戻さないと更新処理中に穴が目立つ状態です。
これを解決するため、チャンクに参照カウンタを取り入れるアイディアを導入します。

導入して、更新中に穴が発生しなくなりました。
こんな感じで使います。

    /// <summary>
    /// マージ対象とその周囲メッシュが作成されたときに呼ばれる
    /// </summary>
    void OnToMergeMesh(Vector3Int chunkKeyXYZ, Vector3Int chunkInt3, int combineCoreChunkIndex, Vector3Int centerChunkInt3)
    {
        if (0 > combineCoreChunkIndex)
        {
            return;
        }
        #region 過去の結合メッシュの開放
        var oldCombineCoreChunks = this.combineCoreChunks[combineCoreChunkIndex];
        if (null != oldCombineCoreChunks)
        {
            const int endSubtractPosition = farChunkRadius * (nearMergeRadius * 2 + 1) - 1;
            foreach (var chunkIndex in oldCombineCoreChunks.chunkIndices)
            {
                // 中心から遠ければチャンクを削除
                var meshFilter = ChunkWorld.worldChunkMeshFilters[chunkIndex];
                if (null == meshFilter)
                {
                    continue;
                }
                var mergeChunkRefInfo = meshFilter.GetComponent<MergeChunkRefInfo>();
                mergeChunkRefInfo.mergeMeshObjectCount -= 1;
                var diff = mergeChunkRefInfo.chunkInt3 - centerChunkInt3;
                var maxSubtractPosition = Mathf.Max(Mathf.Abs(diff.x), Mathf.Abs(diff.y), Mathf.Abs(diff.z));
                // 中央から一定の距離以上のチャンクを削除
                if (endSubtractPosition <= maxSubtractPosition)
                {
                    meshFilter.sharedMesh.Clear();
                    Destroy(meshFilter.gameObject);
                }
                else if (0 == mergeChunkRefInfo.mergeMeshObjectCount)
                {
                    // 参照カウントが無ければ表示
                    meshFilter.GetComponent<MeshRenderer>().enabled = true;
                }
            }
            if (null != oldCombineCoreChunks.combineMeshObject)
            {
                // チャンク結合メッシュの削除
                var oldCombineMeshFilter = oldCombineCoreChunks.combineMeshObject.GetComponent<MeshFilter>();
                oldCombineMeshFilter.sharedMesh.Clear();
                Destroy(oldCombineCoreChunks.combineMeshObject);
            }
        }
        #endregion

        #region マージするメッシュを収集
        int mergeRadius = nearMergeRadius;
        var edgeCount = mergeRadius * 2 + 1;
        var combineCount = edgeCount * edgeCount * edgeCount;
        CombineInstance[] combineInstances = new CombineInstance[combineCount];
        int meshIndex = 0;
        var byteMax = (byte.MaxValue + 1);
        var combineChunks = new CombineChunks();
        for (var x = -mergeRadius; x <= mergeRadius; x++)
        {
            for (var z = -mergeRadius; z <= mergeRadius; z++)
            {
                for (var y = -mergeRadius; y <= mergeRadius; y++)
                {
                    int combineChunkIndex = (byte)(chunkKeyXYZ.x + x) * byteMax * byteMax + (byte)(chunkKeyXYZ.z + z) * byteMax + (byte)(chunkKeyXYZ.y + y);
                    var meshFilter = ChunkWorld.worldChunkMeshFilters[combineChunkIndex];
                    combineChunks.chunkIndices[meshIndex] = combineChunkIndex;

                    CombineInstance combineInstance = new CombineInstance();
                    combineInstance.transform = meshFilter.transform.localToWorldMatrix;
                    combineInstance.subMeshIndex = 0;
                    combineInstance.mesh = meshFilter.sharedMesh;
                    combineInstances[meshIndex++] = combineInstance;
                    // マージ対象のメッシュを非アクティブ化
                    meshFilter.GetComponent<MeshRenderer>().enabled = false;
                    meshFilter.GetComponent<MergeChunkRefInfo>().mergeMeshObjectCount += 1;
                }
            }
        }
        #endregion
        
        // マージオブジェクトの作成
        var meshObject = Instantiate(this.prefabMeshObject, Vector3.zero, Quaternion.identity);
        combineChunks.combineMeshObject = meshObject;
        var combineMeshFilter = meshObject.GetComponent<MeshFilter>();
        // メッシュのマージ
        var mesh = new Mesh();
        mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
        mesh.CombineMeshes(combineInstances, mergeSubMeshes: true, useMatrices: true);
        // メッシュオブジェクトにメッシュを設定
        combineMeshFilter.sharedMesh = mesh;
        #region マテリアルの設定
        if (!this.materials.ContainsKey(this.meshShader.GetInstanceID()))
        {
            var material = new Material(this.meshShader);
            material.SetTexture("Texture2D_5418CE01", textureAlbedo);
            this.materials.Add(this.meshShader.GetInstanceID(), material);
        }
        var renderer = meshObject.GetComponent<MeshRenderer>();
        meshObject.GetComponent<MeshRenderer>().sharedMaterial = this.materials[this.meshShader.GetInstanceID()];
        #endregion

        // 結合メッシュの格納
        this.combineCoreChunks[combineCoreChunkIndex] = combineChunks;
    }

できた。
冒頭の動画になります。

チャンクデータの削除はしないことにした。
あまりにたまり過ぎたら、クリーンにする処理を走らせる方法を取り入れるのもありかも

続きはこちら
simplestar-tech.hatenablog.com

CubeWalkGameなだらかな地形生成と永続化

このシリーズの続きです。
simplestar-tech.hatenablog.com

1.ECS による動的頂点生成と面生成
2.チャンクをまたぐデータ参照
3.キューブの回転とテクスチャの設定

と階段を登ってきました。

今回は過去記事でいうところのこのあたり
simplestar-tech.hatenablog.com
simplestar-tech.hatenablog.com
を整理しながら新しい知見を交えて、地形生成と永続化の手段を記録します。

過去実装からの移植で考えなければならないのは、
1.近景と遠景の階層化
単純に言えば、近場のチャンクにはコライダーが適用され、様々な相互作用の計算が走ります。
遠景のチャンクはチャンク同士でさえ結合され、コライダーは無く、描画にのみ特化した結合メッシュとなります。

加えて、
2.プレイヤーチャンクを中心に、もしプレイヤーがチャンクをまたいで移動した時は、動的に近景チャンクを追加し世界を広げる必要があり、古い近景チャンクは削除

しなければなりません。
そして、遠景と近景の間の穴が見つかることなくうめなければなりません。

もう一つ、
3.複数のマテリアルを利用して世界を表現

できなければなりません。これは描画レートの制約です。
現在の構想では、マテリアル数を 4 つほどに抑えて、オブジェクトを複数に分けるというものでしたが、はたして…

この三点について具体的な実装を考えていきます。

# 近景と遠景の階層化

3次元空間を扱う List の作成となりますが、5ヶ月前はとても難易度の高い立体 List を作ってこれの変化を管理するということを行いました。

難しいテンソル変形操作で、とても休日の片手間でメンテナンスするものではなくなりました。
(これ、休日に管理したくない)

そこで考えたのがソートとキューです。
計算リソースを使うことで、コードを簡略化することにします。

要するに、プレイヤーの座標 + チャンク幅の真ん中オフセットからチャンク位置までの距離を計算し
あたかもチャンクの中心とプレイヤー座標との距離を計算しているかのようにし
その距離を昇順にソートしてキューに詰めます。

キューの先頭から要素を取り出せば、順にプレイヤーに近いチャンクから処理されていくという動きが想像できます。

ぱっと思いつくのは
キューそのものにソート機能は無い
List には Sort 機能ある
何度もソートするなら、先に距離を計算する
チャンク内のキューブ数を増やして、チャンクの数を減らすとソート時間が短縮される

こう考えていくと、最初にリストに乗せるチャンクはどうやって決めるのか定義しなければなりません。

プレイヤーの座標から、チャンクの位置 int, int, int を決定
そこから立方体を切り出すように int, int, int の配列をつくり

これまでの以下のロジックを使って一意のチャンクキーを決定するのはどうか?

        #region 位置からチャンクキーに変換
        var byteMax = (byte.MaxValue + 1);
        var chunkKeyX = chunkPosition.x % byteMax;
        if (0 > chunkKeyX) chunkKeyX += byteMax;
        var chunkKeyY = chunkPosition.y % byteMax;
        if (0 > chunkKeyY) chunkKeyY += byteMax;
        var chunkKeyZ = chunkPosition.z % byteMax;
        if (0 > chunkKeyZ) chunkKeyZ += byteMax;
        #endregion

近景と遠景で処理を分けるとして
遠景はどのように表現すべきか
以前は 2 x 2 x 2 のチャンクのメッシュを合成していた
ただ、難しいのが近景と遠景の接続部分

贅沢なメモリ利用を考えるなら、メッシュ情報をすべてチャンク単位で持ち
近景と重なる遠景の部分は、場合によりチャンク単位で描画され
まったく重ならない領域では、メッシュを結合して 1/8 チャンクのメッシュとなり
さらに遠方では 3 x 3 x 3 で結合して、さらに遠方では 4 x 4 x 4 で結合する
階層は 1, 2, 3, 4, 5 階層くらいに分けて、結合されたチャンクはいつでも柔軟に
チャンク単位でメッシュを結合できる仕組みを考えます。

計算量も有限なので、階層ごとにコンビネーションを取ることは難しいと考えました。

プレイヤーのカメラを内包するチャンクを中心チャンクとして、これが余白をもって変化したタイミングで
最大チャンク半径について、すべてのチャンクの中心チャンクからの距離を計算し、近い順にソートします。

近い順に処理しながら、チャンク単位のメッシュを作成します。
距離が離れていくにつれて、2x2x2, 3x3x3, 4x4x4, 5x5x5 とメッシュを結合していきます。

外周のチャンクを作っている最中はだんだんパフォーマンスが悪くなりそうな予感がしてますが、どうなんでしょう?
ロジックとしては単純で、チャンクの結合が柔軟に行えて良い気がしています。

結合した後、中心チャンクが移動したときはどうなるでしょうか?
もう一度、すべてのチャンクの中心チャンクからの距離を計算し、近い順にソートします。

近い順にメッシュ情報を再利用しながら、一定距離近づいたにも関わらず 2 x 2 x 2 の結合メッシュに所属しているチャンクは一度、その結合メッシュをチャンク単位で描画するように戻し、結合メッシュを破棄

ふとここまでイメージしていて気づいたことに、視錐台カリングのために密集したチャンク同士でメッシュを結合する必要を考えてきたけど
順に取り出した位置はバラバラのチャンクで結合してもパフォーマンスはいくらか上がるという考え方
これなら距離に応じて結合チャンク数を逐次増やしていけるし、順番に結合済みのチャンクを作っていける
外周チャンクを作っているときに大量のチャンク単位のオブジェクトが作られる問題も解決します。

いや、やはり階層化しつつ、密集したチャンクで一つのオブジェクトとしたいところ
プレイヤーカメラからの距離でチャンクのソートが行えたなら、その順番を数で区切って 1階層2階層、3階層とグループ分けできると考えた
2階層のチャンクについて、中心チャンクから一定の間隔で 2 x 2 x 2 の結合を行っていくで良さそう
結合対象から漏れていたらそのチャンクを結合しなければいい
分解ができるなら、周囲のチャンクのみ分解し、その中心チャックに沿って分解と再結合を行っていく
第6階層の走査で、領域に含まれるチャンクについては削除を行うようにして、メモリの肥大を防ぐというもの

プレイヤーカメラと中心チャンクとの距離だけを毎フレーム見ながら、一定距離離れたら、全チャンクとの距離を計算してソートするようにして
いったんどのような動きとなるか考えていきたいと思います。

実装前の最後に、チャンクの情報をどうやってディスクやインターネットから読み込むかを考えてみます。
階層1の処理が終わったら、階層2のチャンクのデータの作成、保存、次回から読み込みを行うようにする。

イメージどおりに動くか、そろそろ実装します。

…一晩寝かせて気づいたのは、階層構造を立体格子状にループさせれば
チャンクまでの距離ソートがいらないのではないか、ということ

ここまで長く書いてきたことは…無かったことにしてください!

例えばこちらが愚直に書き下した、既存のチャンク処理ループ

            #region 作成対象のエンティティを Enqueue
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (var x = -15; x <= 15; x++)
                {
                    for (var z = -15; z <= 15; z++)
                    {
                        for (var y = -15; y <= 15; y++)
                        {
                            this.EnqueueCreateEntity(x, y, z);
                        }
                    }
                }
            }
            #endregion

f:id:simplestar_tech:20190619080304p:plain
31 x 31 x 31 チャンクの生成状況 32.52 ms ループ

f:id:simplestar_tech:20190619080434p:plain
タイムライン

遅い…なんとか高速化できないか

高速化について、次のように、中央から順に周辺チャンクを結合していくループを回すのはどうかと考えた

            #region 作成対象のチャンク情報を Enqueue
            if (Input.GetKeyDown(KeyCode.Space))
            {
                var perChunkRadius = 1;
                for (var x = -perChunkRadius; x <= perChunkRadius; x++)
                {
                    for (var z = -perChunkRadius; z <= perChunkRadius; z++)
                    {
                        for (var y = -perChunkRadius; y <= perChunkRadius; y++)
                        {
                            this.EnqueueCreateEntity(x, y, z, 0);
                        }
                    }
                }
                for (var level = perChunkRadius + 1; level <= 2; level++)
                {
                    if (2 > level)
                    {
                        break;
                    }
                    var offset = 3;
                    var geta = level * offset - 3;
                    for (var x = -geta; x <= geta; x += offset)
                    {
                        for (var z = -geta; z <= geta; z += offset)
                        {
                            for (var y = -geta; y <= geta; y += offset)
                            {
                                if (0 != geta - Mathf.Abs(x) && 0 != geta - Mathf.Abs(y) && 0 != geta - Mathf.Abs(z))
                                {
                                    continue;
                                }
                                for (var radiusX = -perChunkRadius; radiusX <= perChunkRadius; radiusX++)
                                {
                                    for (var radiusZ = -perChunkRadius; radiusZ <= perChunkRadius; radiusZ++)
                                    {
                                        for (var radiusY = -perChunkRadius; radiusY <= perChunkRadius; radiusY++)
                                        {
                                            if (0 == radiusX && 0 == radiusY && 0 == radiusZ)
                                            {
                                                continue;
                                            }
                                            this.EnqueueCreateEntity(x + radiusX, y + radiusY, z + radiusZ, 0);
                                        }
                                    }
                                }
                                this.EnqueueCreateEntity(x, y, z, 1);
                            }
                        }
                    }
                }
            }
            #endregion

f:id:simplestar_tech:20190619230359p:plain
geta 飛び配置で描画負荷を低減

f:id:simplestar_tech:20190621225117p:plain
コアで結合した結果

f:id:simplestar_tech:20190621225225p:plain
描画負荷は十分に解決

プレイヤーのいるチャンクを中心に、処理をループさせて、近景は 1 チャンクずつ
遠景は 3x3x3 を一つのチャンクに結合するという処理は、構想から始めて、だいたいいい形で固まりました。

# 適当な地形生成

プレイヤーカメラ移動に際し、今の巨大なキューブでは移動によって結果変化がわかりません。
このままでは先に進めなくなったので、適当な地形生成を行います。

以前調べた snoise を高さ情報として、なだらかな地形を生成してみます。

イメージを形に…

今はチャンク情報を1チャンク分だけ確保して、すべて同じチャンク情報としています。
これをやめて、すべてのチャンクごとにチャンク情報を保持させるようにします。

見た目は同じでも、中身は別

f:id:simplestar_tech:20190621233319p:plain
見た目が同じでも中身が別

参考に今のコード

    void EnqueueCreateEntity(int x, int y, int z, int margeRadius)
    {
        #region チャンク情報の定義
        var chunkData = new int[ChunkManager.ChunkSizeX * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY];
        var nativeChunkData = new NativeArray<int>(chunkData, Allocator.Persistent);
        CreateWorld(nativeChunkData);
        #endregion

        #region ポインターのポインターにチャンクをセット
        int chunkKeyX, chunkKeyY, chunkKeyZ;
        ChunkInt3ToChunkKey(new Vector3Int(x, y, z), out chunkKeyX, out chunkKeyY, out chunkKeyZ);
        var byteMax = (byte.MaxValue + 1);
        int chunkIndex = chunkKeyX * byteMax * byteMax + chunkKeyZ * byteMax + chunkKeyY;
        this.ppChunkData[chunkIndex] = (int*)nativeChunkData.GetUnsafePtr(); // 本来なら Amazon S3 からダウンロード完了後にこれしてから Enqueue とか?
        this.nativeChunkDataArray[chunkIndex] = nativeChunkData;
        #endregion

        #region キーとなる位置情報を Enqueue
        var chunkPosition = new Vector3Int(x, y, z);
        var createChunkInfo = new CreateChunkInfo { chunkPosition = chunkPosition, mergeRadius = margeRadius };
        this.createEntityQueue.Enqueue(createChunkInfo);
        #endregion
    }

    static void CreateWorld(NativeArray<int> nativeChunkData)
    {
        var pChunkData = (int*)nativeChunkData.GetUnsafePtr();
        for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
        {
            for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
            {
                for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                {
                    var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                    var pData = (byte*)(pChunkData + dataIndex);
                    pData[0] = 1;
                    pData[1] = (byte)Random.Range((int)CubeRotationType.Top000, (int)CubeRotationType.Max);
                    pData[2] = 1;
                    pData[3] = 1;
                }
            }
        }
    }

上記コードの CreateWorld を snoise で作ります。

と、その前に遊ばせている CPU を見つけたので、並列性を高めておきました。

f:id:simplestar_tech:20190622002329p:plain
最低でも論理コア数のオブジェクトを作成するようにして並列計算

f:id:simplestar_tech:20190622111748p:plain
psrnoise 処理 x 1

f:id:simplestar_tech:20190622132048p:plain
pnoise 試験画像

f:id:simplestar_tech:20190622133541p:plain
期待するなだらかな地形

f:id:simplestar_tech:20190622134038j:plain
なだらかな地形

現在の地形生成の実装は次の通り
pnoise の p は periodic (ループする)だそうです。

    static void LoadChunkData(Vector3Int chunkKeyXYZ, Vector3Int chunkPosition, int* pChunkData)
    {
        float amplitude = 64.0f; // 振幅倍率
        float period = 1; // 周期
        float chunkCount = byte.MaxValue + 1;

        for (int x = 0; x < ChunkManager.ChunkSizeX; x++)
        {
            for (int z = 0; z < ChunkManager.ChunkSizeZ; z++)
            {
                float height = 0;
                for (int frequency = 8; frequency < 10; frequency++)
                {
                    var power = Mathf.Pow(2f, frequency);
                    var bAmp = Mathf.Max(amplitude / power, 2);
                    height += bAmp * noise.pnoise(new float2(
                        (chunkKeyXYZ.x * ChunkManager.ChunkSizeX + x) / (float)(ChunkManager.ChunkSizeX * chunkCount),
                        (chunkKeyXYZ.z * ChunkManager.ChunkSizeZ + z) / (float)(ChunkManager.ChunkSizeZ * chunkCount)
                        ) * (period * power), new float2(period * power, period * power));
                }
                for (int y = 0; y < ChunkManager.ChunkSizeY; y++)
                {
                    float cubeHeight = chunkPosition.y * ChunkManager.ChunkSizeY + y;
                    byte sideType = (cubeHeight < height) ? (byte)1 : (byte)0;

                    var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                    var pData = (byte*)(pChunkData + dataIndex);
                    pData[0] = 1;
                    pData[1] = (byte)Random.Range((int)CubeRotationType.Top000, (int)CubeRotationType.Max);
                    pData[2] = sideType;
                    pData[3] = sideType;
                }
            }
        }
    }

# 地形データの永続化

移動を実装する前に、今の地形をファイルにセーブして
次回起動時に、同じキーのチャンク情報が保存されているなら、計算で作らずにファイル読み込みから地形を生成するようにします。

先にポインターでファイルデータをやり取りするクラスを作りました。
参考にした記事はこちら
qiita.com

ちょっとだけ使いたい方向に修正した、ほぼ同じコードがこちら

using System.IO;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.IO.LowLevel.Unsafe;

internal unsafe class UnsafeFileUtility
{
    /// <summary>
    /// 呼び出し元は fileData.Buffer に対し ReleaseReadData で開放する責任あり
    /// </summary>
    public static void ReadData(string filePath, out ReadCommand fileData)
    {
        var fileInfo = new System.IO.FileInfo(filePath);
        long fileSize = fileInfo.Length;

        var readCommand = new NativeArray<ReadCommand>(1, Allocator.Persistent);
        readCommand[0] = new ReadCommand
        {
            Offset = 0,
            Size = fileSize,
            Buffer = UnsafeUtility.Malloc(fileSize, UnsafeUtility.AlignOf<byte>(), Allocator.Persistent),
        };

        var readHandle = AsyncReadManager.Read(filePath, (ReadCommand*)readCommand.GetUnsafePtr(), 1);
        readHandle.JobHandle.Complete();
        fileData = readCommand[0];
        
        readHandle.Dispose();
        readCommand.Dispose();
    }

    /// <summary>
    /// データの書き込み
    /// </summary>
    public static void WriteData(string filePath, ReadCommand fileData)
    {
        byte* pFiledata = (byte*)fileData.Buffer;
        using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        {
            for (int filePosition = 0; filePosition < fileData.Size; filePosition++)
            {
                fs.WriteByte(pFiledata[filePosition]);
            }
        }
    }

    /// <summary>
    /// データの開放
    /// </summary>
    /// <param name="fileData"></param>
    public static void ReleaseReadData(ref ReadCommand fileData)
    {
        if (0 < fileData.Size && null != fileData.Buffer)
        {
            UnsafeUtility.Free(fileData.Buffer, Allocator.Persistent);
            fileData.Buffer = null;
            fileData.Size = 0;
        }
    }
}

これを利用して先程のなだらかな地形生成アルゴリズムとつなげます。
ファイルがあったら読むだけというコードと共に

    /// <summary>
    /// チャンクデータの読み込み(out int* は呼び出し元で開放責任あり)
    /// </summary>
    static void LoadChunkData(Vector3Int chunkKeyXYZ, Vector3Int chunkPosition, out int* pChunkData)
    {
        var fileName = $"{chunkKeyXYZ.x.ToString("000")}-{chunkKeyXYZ.y.ToString("000")}-{chunkKeyXYZ.z.ToString("000")}.bytes";
        var filePath = Path.Combine(Application.persistentDataPath, fileName);
        if (File.Exists(filePath))
        {
            UnsafeFileUtility.ReadData(filePath, out var fileData);
            pChunkData = (int*)fileData.Buffer;
        }
        else
        {
            var fileSize = sizeof(int) * ChunkManager.ChunkSizeX* ChunkManager.ChunkSizeY * ChunkManager.ChunkSizeZ;
            pChunkData = (int*)(UnsafeUtility.Malloc(fileSize, sizeof(int), Allocator.Persistent));

            float amplitude = 64.0f; // 振幅倍率
            float period = 1; // 周期
            float chunkCount = byte.MaxValue + 1;

            for (int x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (int z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    float height = 0;
                    for (int frequency = 8; frequency < 10; frequency++)
                    {
                        var power = Mathf.Pow(2f, frequency);
                        var ampSize = Mathf.Max(amplitude / power, 2);
                        var repSize = period * power;
                        height += ampSize * noise.pnoise(new float2(
                            (chunkKeyXYZ.x * ChunkManager.ChunkSizeX + x) / (float)(ChunkManager.ChunkSizeX * chunkCount),
                            (chunkKeyXYZ.z * ChunkManager.ChunkSizeZ + z) / (float)(ChunkManager.ChunkSizeZ * chunkCount)
                            ) * repSize, new float2(repSize, repSize));
                    }
                    for (int y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        float cubeHeight = chunkPosition.y * ChunkManager.ChunkSizeY + y;
                        byte sideType = (cubeHeight < height) ? (byte)1 : (byte)0;

                        var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                        var pData = (byte*)(pChunkData + dataIndex);
                        pData[0] = 1;
                        pData[1] = (byte)Random.Range((int)CubeRotationType.Top000, (int)CubeRotationType.Max);
                        pData[2] = sideType;
                        pData[3] = sideType;
                    }
                }
            }
            UnsafeFileUtility.WriteData(filePath, new Unity.IO.LowLevel.Unsafe.ReadCommand { Buffer = pChunkData, Size = fileSize });
        }
    }

問題なく動きました。

f:id:simplestar_tech:20190622165740p:plain
なだらかな地形を永続化

CubeWalkGameなだらかな地形生成と永続化ができました。
次のプレイヤーの移動については、長くなったので記事を分けます。

# プレイヤーチャンクの特定

# プレイヤーチャンク移動タイミングの定義

# 既存のチャンクメッシュを利用する再結合処理

続きはこちら
simplestar-tech.hatenablog.com

Unity:CubeWalkGameのキューブのテクスチャ座標と回転

# まえがき

これらの記事シリーズの続きです。
今回は作業メモになっているので、開発者以外が読むと辛い文章かも…

simplestar-tech.hatenablog.com
simplestar-tech.hatenablog.com


シリーズ名は CubeWalkGame として、前回はチャンクをまたぐキューブ情報の取得を確認しました。
前回はマクロ視点でしたが、今回はミクロ視点ということで、キューブ単体のテクスチャ座標と回転表現をやります。

## 無回転でテクスチャ貼り

テクスチャ座標はこれなんだけど、どうやって貼り付け?

new Vector2[]{
            // Top
            new Vector2 (0, 1),
            new Vector2 (1, 1),
            new Vector2 (1, 0),
            // Bottom
            new Vector2 (1, 0),
            new Vector2 (0, 0),
            new Vector2 (0, 1),
            // Right up
            new Vector2 (1, 1),
            new Vector2 (2, 1),
            new Vector2 (2, 0),
            // Right down
            new Vector2 (2, 0),
            new Vector2 (1, 0),
            new Vector2 (1, 1),
            // Forward up
            new Vector2 (2, 1),
            new Vector2 (3, 1),
            new Vector2 (3, 0),
            // Forward down
            new Vector2 (3, 0),
            new Vector2 (2, 0),
            new Vector2 (2, 1),
            // Cross up
            new Vector2 (3, 1),
            new Vector2 (4.4142f, 1),
            new Vector2 (4.4142f, 0),
            // Cross down
            new Vector2 (4.4142f, 0),
            new Vector2 (3, 0),
            new Vector2 (3, 1),
        }

ひとまず頂点位置と同じようにセットしてみます。

マジックコードを発見しましたが、コレなんでしょう?

            const float mtf = 232f / 4096f;
            float mtoffu = col / 4.0f;
            float mtoffv = mtf * (16 - row % 17) % 17 + 0.0371f;


mesh.uv[vertCount * 2 + 0] = uvSrc[i * 2 + 0] * mtf + mtoffu;
mesh.uv[vertCount * 2 + 1] = uvSrc[i * 2 + 1] * mtf + mtoffv;

こちらの level 4 との対となるマジックコードでした。
github.com

f:id:simplestar_tech:20190616123849p:plain
コードを復元するように書き込むとこの通り

ロジックの肝心な箇所はこちら

                            int row = 10;
                            int col = 0;
                            const float mtf = 232f / 4096f;
                            float mtoffu = col / 4.0f;
                            float mtoffv = mtf * (16 - row % 17) % 17 + 0.0371f;
                            for (int vertexIndex = 0; vertexIndex < vertexCount * 2; vertexIndex += 2)
                            {
                                var uvXIndex = uvOffset + vertexIndex;
                                meshData.pUVVector2[uvXIndex + 0] *= mtf;
                                meshData.pUVVector2[uvXIndex + 0] += mtoffu;
                                meshData.pUVVector2[uvXIndex + 1] *= mtf;
                                meshData.pUVVector2[uvXIndex + 1] += mtoffv;
                            }
                            verticesOffset += vertexCount * 3;
                            uvOffset += vertexCount * 2;

いずれテクスチャの row, col の値は ChunkData から取らないといけませんね。

## 頂点を回転

この記事で試しておかなければならないこと、それが mathematics でその場で頂点回転できないか?というもの
キューブの頂点座標は原点中心で定義しているので、90度または パイ/2 ラジアンで回転する quaternion とかあれば良いのですが

ECS で使う Mathematics を確認します。

こういうときは過去記事を引っ張ってきて、関連リンクを読み漁ります。
simplestar-tech.hatenablog.com

そして発見!

using static Unity.Mathematics.math;

        /// <summary>Returns the result of rotating a vector by a unit quaternion.</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static float3 rotate(quaternion q, float3 v)
        {
            float3 t = 2 * cross(q.value.xyz, v);
            return v + q.value.w * t + cross(q.value.xyz, t);
        }

仮で、すべての頂点を X 軸で 90 度回転させてみます。

コードはこんな感じかな?

                            for (int vertexIndex = 0; vertexIndex < vertexCount * 3; vertexIndex += 3)
                            {
                                var positionXIndex = verticesOffset + vertexIndex;
                                var pPosition = meshData.pVerticesVector3 + positionXIndex;

                                var q = Unity.Mathematics.quaternion.RotateX(PI / 2);
                                var p = float3(pPosition[0], pPosition[1], pPosition[2]);
                                var r = rotate(q, p);

                                pPosition[0] = r.x + x * ChunkManager.CubeSide;
                                pPosition[1] = r.y + y * ChunkManager.CubeSide;
                                pPosition[2] = r.z + z * ChunkManager.CubeSide;
                            }

変化は?

f:id:simplestar_tech:20190616140407p:plain
x軸で時計回りに90度回転しました

期待通りですね!

回転ごとに周辺チャンクの参照先を変えなければならないので、ここが頑張りどころです。

## 回転タイプを byte 値で判別

ここは強引にこう定義してみました。

internal enum CubeRotationType : byte
{
    Top000 = 0,
    Top090,
    Top180,
    Top270,

    Bottom000,
    Bottom090,
    Bottom180,
    Bottom270,

    Forward000,
    Forward090,
    Forward180,
    Forward270,

    Back000,
    Back090,
    Back180,
    Back270,

    Right000,
    Right090,
    Right180,
    Right270,

    Left000,
    Left090,
    Left180,
    Left270,

    Max
}

以前の回転結果図を参考に決めてみたけど、これでうまくいくか

   /// <summary>
    /// byte の回転タイプから Quaternion を作って返す
    /// </summary>
    static Unity.Mathematics.quaternion GetRotationQuaternion(CubeRotationType rotationType)
    {
        // @TODO: インデックスアクセスに
        var rotationQuaternion = Unity.Mathematics.quaternion.identity;
        switch (rotationType)
        {
            case CubeRotationType.Top000:
                break;
            case CubeRotationType.Top090:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateY(PI / 2);
                break;
            case CubeRotationType.Top180:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateY(PI);
                break;
            case CubeRotationType.Top270:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateY(PI * 3 / 2);
                break;
            case CubeRotationType.Bottom000:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateX(PI);
                break;
            case CubeRotationType.Bottom090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Bottom180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Bottom270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Forward000:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Forward090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Forward180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(0));
                break;
            case CubeRotationType.Forward270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Back000:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateX(-PI / 2);
                break;
            case CubeRotationType.Back090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Back180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Back270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Right000:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Right090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(0));
                break;
            case CubeRotationType.Right180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Right270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Left000:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Left090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Left180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Left270:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateZ(PI / 2);
                break;
            case CubeRotationType.Max:
                break;
            default:
                break;
        }
        return rotationQuaternion;
    }

次に周辺ブロックを見るためのインデックスを回転タイプから求めます。

いっぱい switch 文を書いていて気づく、enum が巨大な連番だったら switch 文の分岐書くより
配列のインデックスアクセスにした方が、結果取得早いしプログラミングも楽です。(読解し辛いのでコメント必須ですが)

前回のポインターポインターを活用することにして、次の通り定義してみました。

        #region 回転タイプと面タイプごとの方向
        var top = int3(0, 1, 0);
        var bottom = int3(0, -1, 0);
        var right = int3(1, 0, 0);
        var left = int3(-1, 0, 0);
        var forward = int3(0, 0, 1);
        var back = int3(0, 0, -1);
        this.ppRotationFaceDirections = (int3**)(UnsafeUtility.Malloc(sizeof(int3*) * (int)CubeRotationType.Max, sizeof(int3*), Allocator.Persistent));
        #region Top
        this.ppRotationFaceDirections[(int)CubeRotationType.Top000] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Top000] = new NativeArray<int3>(
            new int3[] {
                top,
                bottom,
                right,
                forward,
                bottom,
                top,
                left,
                back }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Top090] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Top090] = new NativeArray<int3>(
            new int3[] {
                top,
                bottom,
                back,
                right,
                bottom,
                top,
                forward,
                left }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Top180] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Top180] = new NativeArray<int3>(
            new int3[] {
                top,
                bottom,
                left,
                back,
                bottom,
                top,
                right,
                forward }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Top270] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Top270] = new NativeArray<int3>(
            new int3[] {
                top,
                bottom,
                forward,
                left,
                bottom,
                top,
                back,
                right }, Allocator.Persistent)).GetUnsafePtr();
        #endregion
        #region Bottom
        this.ppRotationFaceDirections[(int)CubeRotationType.Bottom000] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Bottom000] = new NativeArray<int3>(
            new int3[] {
                bottom,
                top,
                right,
                back,
                top,
                bottom,
                left,
                forward }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Bottom090] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Bottom090] = new NativeArray<int3>(
            new int3[] {
                bottom,
                top,
                forward,
                right,
                top,
                bottom,
                back,
                left }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Bottom180] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Bottom180] = new NativeArray<int3>(
            new int3[] {
                bottom,
                top,
                left,
                forward,
                top,
                bottom,
                right,
                back }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Bottom270] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Bottom270] = new NativeArray<int3>(
            new int3[] {
                bottom,
                top,
                back,
                left,
                top,
                bottom,
                forward,
                right }, Allocator.Persistent)).GetUnsafePtr();
        #endregion
        #region Forward
        this.ppRotationFaceDirections[(int)CubeRotationType.Forward000] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Forward000] = new NativeArray<int3>(
            new int3[] {
                forward,
                back,
                left,
                top,
                back,
                forward,
                right,
                bottom }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Forward090] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Forward090] = new NativeArray<int3>(
            new int3[] {
                forward,
                back,
                bottom,
                left,
                back,
                forward,
                top,
                right }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Forward180] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Forward180] = new NativeArray<int3>(
            new int3[] {
                forward,
                back,
                right,
                bottom,
                back,
                forward,
                left,
                top }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Forward270] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Forward270] = new NativeArray<int3>(
            new int3[] {
                forward,
                back,
                top,
                right,
                back,
                forward,
                bottom,
                left }, Allocator.Persistent)).GetUnsafePtr();
        #endregion
        #region Back
        this.ppRotationFaceDirections[(int)CubeRotationType.Back000] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Back000] = new NativeArray<int3>(
            new int3[] {
                back,
                forward,
                right,
                top,
                forward,
                back,
                left,
                bottom }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Back090] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Back090] = new NativeArray<int3>(
            new int3[] {
                back,
                forward,
                bottom,
                right,
                forward,
                back,
                top,
                left }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Back180] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Back180] = new NativeArray<int3>(
            new int3[] {
                back,
                forward,
                left,
                bottom,
                forward,
                back,
                right,
                top }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Back270] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Back270] = new NativeArray<int3>(
            new int3[] {
                back,
                forward,
                top,
                left,
                forward,
                back,
                bottom,
                right }, Allocator.Persistent)).GetUnsafePtr();
        #endregion
        #region Right
        this.ppRotationFaceDirections[(int)CubeRotationType.Right000] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Right000] = new NativeArray<int3>(
            new int3[] {
                right,
                left,
                forward,
                top,
                left,
                right,
                back,
                bottom }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Right090] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Right090] = new NativeArray<int3>(
            new int3[] {
                right,
                left,
                bottom,
                forward,
                left,
                right,
                top,
                back }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Right180] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Right180] = new NativeArray<int3>(
            new int3[] {
                right,
                left,
                back,
                bottom,
                left,
                right,
                forward,
                top }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Right270] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Right270] = new NativeArray<int3>(
            new int3[] {
                right,
                left,
                top,
                back,
                left,
                right,
                bottom,
                forward }, Allocator.Persistent)).GetUnsafePtr();
        #endregion
        #region Left
        this.ppRotationFaceDirections[(int)CubeRotationType.Left000] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Left000] = new NativeArray<int3>(
            new int3[] {
                left,
                right,
                back,
                top,
                right,
                left,
                forward,
                bottom }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Left090] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Left090] = new NativeArray<int3>(
            new int3[] {
                left,
                right,
                bottom,
                back,
                right,
                left,
                top,
                forward }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Left180] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Left180] = new NativeArray<int3>(
            new int3[] {
                left,
                right,
                forward,
                bottom,
                right,
                left,
                back,
                top }, Allocator.Persistent)).GetUnsafePtr();
        this.ppRotationFaceDirections[(int)CubeRotationType.Left270] = (int3*)(this.rotationFaceDirectionArray[(int)CubeRotationType.Left270] = new NativeArray<int3>(
            new int3[] {
                left,
                right,
                top,
                forward,
                right,
                left,
                bottom,
                back }, Allocator.Persistent)).GetUnsafePtr();
        #endregion
        #endregion
    }

    protected override void OnDestroy()
    {
        for (int cubeRotationIndex = 0; cubeRotationIndex < (int)CubeRotationType.Max; cubeRotationIndex++)
        {
            this.rotationFaceDirectionArray[cubeRotationIndex].Dispose();
        }
        UnsafeUtility.Free(this.ppRotationFaceDirections, Allocator.Persistent);

こちらのポインターポインターを参照することで、キューブの回転タイプから正しい方向の周囲のキューブ情報の取得が行えることが確認できました。

頂点情報を作成するコードをリファクタリングして、同じようなコードをまとめた結果がこちら

    /// <summary>
    /// 頂点バッファに頂点データをコピー書き込み
    /// </summary>
    [BurstCompile]
    unsafe struct CopyWriteVerticesJob : IJobParallelFor
    {
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal int3** ppRotationFaceDirection;
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pVerticesSource;
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pUVSource;
        [ReadOnly] internal int sourceCount;
        internal NativeArray<ChunkMeshData> meshDataArray;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            this.verticesOffset = 0;
            this.uvOffset = 0;
            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var pData = GetDataPtr(meshData, x, y, z);
                        if (1 == pData[(int)ChunkDataType.Category])
                        {
                            var rotationType = (CubeRotationType)pData[(int)ChunkDataType.Rotation];

                            this.sourceOffset = 0;
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.TopA);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.BottomA);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.RightA);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.ForwardA);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.CrossA);

                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.BottomB);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.TopB);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.LeftB);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.BackB);
                            this.MakeFace(meshData, x, y, z, rotationType, CubeFaceType.CrossB);
                        }
                    }
                }
            }
        }

        private int MakeFace(ChunkMeshData meshData, int x, int y, int z,
            CubeRotationType rotationType, CubeFaceType faceType)
        {
            int faceCount = 0;
            int dataSideIndex = -1;
            int crossFlag = 0;
            #region 面タイプごとの変数を決定
            switch (faceType)
            {
                case CubeFaceType.TopA:                    
                case CubeFaceType.BottomA:
                    {
                        dataSideIndex = (int)ChunkDataType.SideA;
                        faceCount = 1;
                    }
                    break;
                case CubeFaceType.BottomB:
                case CubeFaceType.TopB:
                    {
                        dataSideIndex = (int)ChunkDataType.SideB;
                        faceCount = 1;
                    }
                    break;
                case CubeFaceType.RightA:
                case CubeFaceType.ForwardA:
                    {
                        dataSideIndex = (int)ChunkDataType.SideA;
                        faceCount = 2;
                    }
                    break;
                case CubeFaceType.LeftB:
                case CubeFaceType.BackB:
                    {
                        dataSideIndex = (int)ChunkDataType.SideB;
                        faceCount = 2;
                    }
                    break;
                case CubeFaceType.CrossA:
                    {
                        dataSideIndex = (int)ChunkDataType.SideA;
                        faceCount = 2;
                        crossFlag = 1;
                    }
                    break;
                case CubeFaceType.CrossB:
                    {
                        dataSideIndex = (int)ChunkDataType.SideB;
                        faceCount = 2;
                        crossFlag = 1;
                    }
                    break;
                default:
                    break;
            }
            #endregion

            var vertexCount = 0;
            #region 周囲チャンクデータをチェックして、面作成が必要なときのみコピー
            var sideCubeOffset = crossFlag == 0 ? this.ppRotationFaceDirection[(int)rotationType][(int)faceType] : int3(0, 0, 0);
            var pData = GetDataPtr(meshData, x + sideCubeOffset.x, y + sideCubeOffset.y, z + sideCubeOffset.z);
            if (!(null != pData && 1 == pData[(int)ChunkDataType.Category] && 1 == pData[dataSideIndex]))
            {
                UnsafeUtility.MemCpy(meshData.pVerticesVector3 + this.verticesOffset, this.pVerticesSource + this.sourceOffset * 9, size: 9 * sizeof(float) * faceCount);
                UnsafeUtility.MemCpy(meshData.pUVVector2 + this.uvOffset, this.pUVSource + this.sourceOffset * 6, size: 6 * sizeof(float) * faceCount);
                vertexCount = faceCount * 3;
            }
            this.sourceOffset += faceCount;
            #endregion

            #region コピーしたものを回転タイプとチャンク内インデックスから、正しい頂点位置へ移動
            for (int vertexIndex = 0; vertexIndex < vertexCount * 3; vertexIndex += 3)
            {
                // コピーした頂点情報を取得
                var pPosition = meshData.pVerticesVector3 + this.verticesOffset + vertexIndex;
                // 回転タイプによる回転
                var quaternion = GetRotationQuaternion(rotationType);
                var position = float3(pPosition[0], pPosition[1], pPosition[2]);
                var resultPosition = rotate(quaternion, position);
                // チャンク内オフセットを足して格納
                pPosition[0] = resultPosition.x + x * ChunkManager.CubeSide;
                pPosition[1] = resultPosition.y + y * ChunkManager.CubeSide;
                pPosition[2] = resultPosition.z + z * ChunkManager.CubeSide;
            }
            this.verticesOffset += vertexCount * 3;
            #endregion

            #region UV 設定
            int row = 10; // データから決めるべき
            int col = 0;
            const float mtf = 232f / 4096f;
            float mtoffu = col / 4.0f;
            float mtoffv = mtf * (16 - row % 17) % 17 + 0.0371f;
            for (int vertexIndex = 0; vertexIndex < vertexCount * 2; vertexIndex += 2)
            {
                var pUV = meshData.pUVVector2 + this.uvOffset + vertexIndex;
                pUV[0] = pUV[0] * mtf + mtoffu;
                pUV[1] = pUV[1] * mtf + mtoffv;
            }
            this.uvOffset += vertexCount * 2;
            #endregion

            return vertexCount;
        }

        int sourceOffset;
        int verticesOffset;
        int uvOffset;
    }

このジョブの関数の面を描くべきかどうかの判定は再利用して頂点カウントのジョブ側でも使いたいところ
切り出し方を考えてみると

具体的には vertexCount を計算する部分を共通化できるといい

できた。

言葉にし難いけど、System は以下の通り、だいぶコンパクトにたたむことができた

using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using static Unity.Mathematics.math;

internal unsafe class CreateMeshSystem : ComponentSystem
{
    /// <summary>
    /// チャンク情報から頂点数をカウント
    /// </summary>
    [BurstCompile]
    unsafe struct CountVerticesJob : IJobParallelFor
    {
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal int3** ppRotationFaceDirection;
        internal NativeArray<ChunkMeshData> meshDataArray;
        internal int sourceCount;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            meshData.vertexCount = 0;
            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var pData = GetDataPtr(meshData, x, y, z);
                        if (1 == pData[(int)ChunkDataType.Category]) // @debug 1 とは?
                        {
                            var rotationType = (CubeRotationType)pData[(int)ChunkDataType.Rotation];
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.TopA).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.BottomA).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.RightA).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.ForwardA).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.CrossA).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.BottomB).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.TopB).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.LeftB).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.BackB).activeVertexCount;
                            meshData.vertexCount += CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.CrossB).activeVertexCount;
                        }
                    }
                }
            }
            this.meshDataArray[entityIndex] = meshData;
        }
    }

    /// <summary>
    /// 頂点バッファに頂点データをコピー書き込み
    /// </summary>
    [BurstCompile]
    unsafe struct CopyWriteVerticesJob : IJobParallelFor
    {
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal int3** ppRotationFaceDirection;
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pVerticesSource;
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pUVSource;
        internal NativeArray<ChunkMeshData> meshDataArray;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            this.verticesOffset = 0;
            this.uvOffset = 0;
            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var pData = GetDataPtr(meshData, x, y, z);
                        if (1 == pData[(int)ChunkDataType.Category]) // @debug 1 とは?
                        {
                            var rotationType = (CubeRotationType)pData[(int)ChunkDataType.Rotation];

                            this.sourceFaceOffset = 0;
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.TopA));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.BottomA));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.RightA));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.ForwardA));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.CrossA));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.BottomB));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.TopB));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.LeftB));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.BackB));
                            this.CalculateMeshData(meshData, x, y, z, rotationType, CountFace(meshData, x, y, z, rotationType, this.ppRotationFaceDirection, CubeFaceType.CrossB));
                        }
                    }
                }
            }
        }

        /// <summary>
        /// メッシュ情報をコピー&計算
        /// </summary>
        private void CalculateMeshData(ChunkMeshData meshData, int x, int y, int z, CubeRotationType rotationType, CountData countData)
        {
            if (0 < countData.activeVertexCount)
            {
                // 作成対象の頂点データをコピー
                UnsafeUtility.MemCpy(meshData.pVerticesVector3 + this.verticesOffset, this.pVerticesSource + 9 * this.sourceFaceOffset, size: 3 * sizeof(float) * countData.activeVertexCount);
                UnsafeUtility.MemCpy(meshData.pUVVector2 + this.uvOffset, this.pUVSource + 6 * this.sourceFaceOffset, size: 2 * sizeof(float) * countData.activeVertexCount);

                #region コピーしたものを回転タイプとチャンク内インデックスから、正しい頂点位置へ移動
                for (int vertexIndex = 0; vertexIndex < countData.activeVertexCount * 3; vertexIndex += 3)
                {
                    // コピーした頂点情報を取得
                    var pPosition = meshData.pVerticesVector3 + this.verticesOffset + vertexIndex;
                    // 回転タイプによる回転
                    var quaternion = GetRotationQuaternion(rotationType);
                    var position = float3(pPosition[0], pPosition[1], pPosition[2]);
                    var resultPosition = rotate(quaternion, position);
                    // チャンク内オフセットを足して格納
                    pPosition[0] = resultPosition.x + x * ChunkManager.CubeSide;
                    pPosition[1] = resultPosition.y + y * ChunkManager.CubeSide;
                    pPosition[2] = resultPosition.z + z * ChunkManager.CubeSide;
                }
                this.verticesOffset += countData.activeVertexCount * 3;
                #endregion

                #region UV 設定
                int row = 10; // @debug データから決めるべき あと変数名キモ
                int col = 0;
                const float mtf = 232f / 4096f;
                float mtoffu = col / 4.0f;
                float mtoffv = mtf * (16 - row % 17) % 17 + 0.0371f;
                for (int vertexIndex = 0; vertexIndex < countData.activeVertexCount * 2; vertexIndex += 2)
                {
                    var pUV = meshData.pUVVector2 + this.uvOffset + vertexIndex;
                    pUV[0] = pUV[0] * mtf + mtoffu;
                    pUV[1] = pUV[1] * mtf + mtoffv;
                }
                this.uvOffset += countData.activeVertexCount * 2;
                #endregion
            }
            // 作成しなくても面タイプごとの面数オフセットを加算
            this.sourceFaceOffset += countData.reservedFaceOffset;
        }

        int sourceFaceOffset;
        int verticesOffset;
        int uvOffset;
    }

    /// <summary>
    /// 回転タイプと面タイプとチャンク内の座標から、作成するべき頂点数をカウント
    /// </summary>
    static CountData CountFace(ChunkMeshData meshData, int x, int y, int z,
            CubeRotationType rotationType, int3** ppRotationFaceDirection, CubeFaceType faceType)
    {
        int faceCount = 0;
        int dataSideIndex = -1;
        int crossFlag = 0;
        #region 面タイプごとの変数を決定
        switch (faceType)
        {
            case CubeFaceType.TopA:
            case CubeFaceType.BottomA:
                {
                    dataSideIndex = (int)ChunkDataType.SideA;
                    faceCount = 1;
                }
                break;
            case CubeFaceType.BottomB:
            case CubeFaceType.TopB:
                {
                    dataSideIndex = (int)ChunkDataType.SideB;
                    faceCount = 1;
                }
                break;
            case CubeFaceType.RightA:
            case CubeFaceType.ForwardA:
                {
                    dataSideIndex = (int)ChunkDataType.SideA;
                    faceCount = 2;
                }
                break;
            case CubeFaceType.LeftB:
            case CubeFaceType.BackB:
                {
                    dataSideIndex = (int)ChunkDataType.SideB;
                    faceCount = 2;
                }
                break;
            case CubeFaceType.CrossA:
                {
                    dataSideIndex = (int)ChunkDataType.SideB;
                    faceCount = 2;
                    crossFlag = 1;
                }
                break;
            case CubeFaceType.CrossB:
                {
                    dataSideIndex = (int)ChunkDataType.SideA;
                    faceCount = 2;
                    crossFlag = 1;
                }
                break;
            default:
                break;
        }
        #endregion

        #region 周囲チャンクデータをチェックして、面作成が必要なときのみコピー
        var vertexCount = 0;
        var sideCubeOffset = crossFlag == 0 ? ppRotationFaceDirection[(int)rotationType][(int)faceType] : int3(0, 0, 0);
        var pData = GetDataPtr(meshData, x + sideCubeOffset.x, y + sideCubeOffset.y, z + sideCubeOffset.z);
        if (!(null != pData && 1 == pData[(int)ChunkDataType.Category] && 1 == pData[dataSideIndex])) // @debug 1 とは? 相手の回転も見ないと駄目なのでは?
        {
            vertexCount = faceCount * 3;
        }
        #endregion
        return new CountData { activeVertexCount = vertexCount, reservedFaceOffset = faceCount };
    }

    static byte* GetDataPtr(ChunkMeshData meshData, int x, int y, int z)
    {
        if (0 <= x && ChunkManager.ChunkSizeX > x
            && 0 <= y && ChunkManager.ChunkSizeY > y
            && 0 <= z && ChunkManager.ChunkSizeZ > z)
        {
            #region チャンク内のデータを返す
            var byteMax = (byte.MaxValue + 1);
            int chunkIndex = meshData.chunkKeyX * byteMax * byteMax + meshData.chunkKeyZ * byteMax + meshData.chunkKeyY;
            var pChunkData = meshData.ppChunkData[chunkIndex];
            var dataIndex = x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y;
            return (byte*)(pChunkData + dataIndex);
            #endregion
        }
        else
        {
            #region チャンク越えカウント
            var overChunkCountX = x / ChunkManager.ChunkSizeX;
            if (0 > x) overChunkCountX -= 1;
            var overChunkCountY = y / ChunkManager.ChunkSizeY;
            if (0 > y) overChunkCountY -= 1;
            var overChunkCountZ = z / ChunkManager.ChunkSizeZ;
            if (0 > z) overChunkCountZ -= 1;
            #endregion
            #region byte オーバーフローによる値ループ
            var chunkKeyX = (byte)(meshData.chunkKeyX + overChunkCountX);
            var chunkKeyY = (byte)(meshData.chunkKeyY + overChunkCountY);
            var chunkKeyZ = (byte)(meshData.chunkKeyZ + overChunkCountZ);
            #endregion
            #region チャンクの特定
            var byteMax = (byte.MaxValue + 1);
            int chunkIndex = chunkKeyX * byteMax * byteMax + chunkKeyZ * byteMax + chunkKeyY;
            var pChunkData = meshData.ppChunkData[chunkIndex];
            #endregion
            if (null != pChunkData)
            {
                #region チャンク内のデータインデックスへ変換
                x -= overChunkCountX * ChunkManager.ChunkSizeX;
                y -= overChunkCountY * ChunkManager.ChunkSizeY;
                z -= overChunkCountZ * ChunkManager.ChunkSizeZ;
                var dataIndex = x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y;
                #endregion
                return (byte*)(pChunkData + dataIndex);
            }

        }
        return null;
    }

   /// <summary>
    /// byte の回転タイプから Quaternion を作って返す
    /// </summary>
    static Unity.Mathematics.quaternion GetRotationQuaternion(CubeRotationType rotationType)
    {
        // @TODO: インデックスアクセスに
        var rotationQuaternion = Unity.Mathematics.quaternion.identity;
        switch (rotationType)
        {
            case CubeRotationType.Top000:
                break;
            case CubeRotationType.Top090:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateY(PI / 2);
                break;
            case CubeRotationType.Top180:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateY(PI);
                break;
            case CubeRotationType.Top270:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateY(PI * 3 / 2);
                break;
            case CubeRotationType.Bottom000:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateX(PI);
                break;
            case CubeRotationType.Bottom090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Bottom180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Bottom270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Forward000:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Forward090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Forward180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(0));
                break;
            case CubeRotationType.Forward270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Back000:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateX(-PI / 2);
                break;
            case CubeRotationType.Back090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Back180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Back270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateX(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Right000:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Right090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(0));
                break;
            case CubeRotationType.Right180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Right270:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(-PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Left000:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(PI / 2), Unity.Mathematics.quaternion.RotateY(PI / 2));
                break;
            case CubeRotationType.Left090:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(PI / 2), Unity.Mathematics.quaternion.RotateY(PI));
                break;
            case CubeRotationType.Left180:
                rotationQuaternion = mul(Unity.Mathematics.quaternion.RotateZ(PI / 2), Unity.Mathematics.quaternion.RotateY(PI * 3 / 2));
                break;
            case CubeRotationType.Left270:
                rotationQuaternion = Unity.Mathematics.quaternion.RotateZ(PI / 2);
                break;
            case CubeRotationType.Max:
                break;
            default:
                break;
        }
        return rotationQuaternion;
    }

    protected override unsafe void OnCreate()
    {
        base.OnCreate();

        this.query = GetEntityQuery(new EntityQueryDesc
        {
            All = new[] { ComponentType.ReadOnly<CreateMeshMarker>(),
                ComponentType.ReadWrite<ChunkMeshData>() },
        });

        #region モデルの形状を定義→NativeArray確保
        // 省略
        #endregion
    }

    protected override void OnDestroy()
    {
        for (int cubeRotationIndex = 0; cubeRotationIndex < (int)CubeRotationType.Max; cubeRotationIndex++)
        {
            this.rotationFaceDirectionArray[cubeRotationIndex].Dispose();
        }
        UnsafeUtility.Free(this.ppRotationFaceDirection, Allocator.Persistent);

        this.nativeUVSource.Dispose();
        this.nativeVerticesSource.Dispose();

        base.OnDestroy();
    }

    protected unsafe override void OnUpdate()
    {
        #region NativeArray 確保
        var entities = this.query.ToEntityArray(Allocator.TempJob);
        var meshDataArray = this.query.ToComponentDataArray<ChunkMeshData>(Allocator.TempJob);
        #endregion

        #region メッシュの頂点数をカウント
        var countVerticesJob = new CountVerticesJob
        {
            ppRotationFaceDirection = this.ppRotationFaceDirection,
            sourceCount = this.nativeVerticesSource.Length,
            meshDataArray = meshDataArray
        };
        var countJobHandle = countVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        countJobHandle.Complete();
        #endregion

        #region カウント数→頂点バッファを確保→バッファポインタを ComponentData に代入
        this.entityMeshDataList.Clear();
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var meshData = meshDataArray[entityIndex];
            var vertexCount = meshData.vertexCount;
            var entityMeshData = new EntityMeshData {
                nativeVertices = new NativeArray<Vector3>(vertexCount, Allocator.TempJob),
                nativeUV = new NativeArray<Vector2>(vertexCount, Allocator.TempJob),
            };
            this.entityMeshDataList.Add(entityMeshData);
            meshData.pVerticesVector3 = (float*)entityMeshData.nativeVertices.GetUnsafePtr();
            meshData.pUVVector2 = (float*)entityMeshData.nativeUV.GetUnsafePtr();
            meshDataArray[entityIndex] = meshData;
        }
        #endregion

        #region 頂点バッファに頂点データをコピー
        var copyVerticesJob = new CopyWriteVerticesJob
        {
            ppRotationFaceDirection = this.ppRotationFaceDirection,
            pVerticesSource = this.pVerticesSource,
            pUVSource = this.pUVSource,
            meshDataArray = meshDataArray
        };
        var copyJobHandle = copyVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        copyJobHandle.Complete();
        #endregion

        #region 頂点バッファ→マネージド配列→メッシュ作成→メッシュ法線・接線の計算
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            var entityMeshData = this.entityMeshDataList[entityIndex];
            var vertices = entityMeshData.nativeVertices.ToArray();
            var uv = entityMeshData.nativeUV.ToArray();

            var mesh = new Mesh();
            mesh.Clear();
            mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
            mesh.vertices = vertices;
            mesh.uv = uv;
            int[] triangles = new int[vertices.Length];
            for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
            {
                triangles[vertexIndex] = vertexIndex;
            }
            mesh.SetIndices(triangles, MeshTopology.Triangles, submesh: 0, calculateBounds: true);
            mesh.RecalculateNormals();
            mesh.RecalculateTangents();
            var meshFilter = EntityManager.GetComponentObject<MeshFilter>(entity);
            meshFilter.mesh = mesh;

            entityMeshData.nativeUV.Dispose();
            entityMeshData.nativeVertices.Dispose();
        }
        this.entityMeshDataList.Clear();
        #endregion

        #region entity から marker の除去
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            EntityManager.RemoveComponent(entity, ComponentType.ReadOnly<CreateMeshMarker>());
        }
        #endregion

        #region NativeArray 開放
        meshDataArray.Dispose();
        entities.Dispose();
        #endregion
    }

    EntityQuery query;
    
    NativeArray<float> nativeVerticesSource;
    float* pVerticesSource;
    NativeArray<float> nativeUVSource;
    float* pUVSource;

    List<EntityMeshData> entityMeshDataList = new List<EntityMeshData>();

    NativeArray<int3>[] rotationFaceDirectionArray = new NativeArray<int3>[(int)CubeRotationType.Max];
    int3** ppRotationFaceDirection;


    class EntityMeshData
    {
        public NativeArray<Vector3> nativeVertices;
        public NativeArray<Vector2> nativeUV;
    }

    struct CountData
    {
        public int activeVertexCount;
        public int reservedFaceOffset;
    }
}

internal enum ChunkDataType : int
{
    Category = 0,
    Rotation,
    SideA,
    SideB,
    Max
}

internal enum CubeFaceType : byte
{
    TopA = 0,
    BottomA,
    RightA,
    ForwardA,
    BottomB,
    TopB,
    LeftB,
    BackB,
    CrossA,
    CrossB,
    Max,
}

internal enum CubeRotationType : byte
{
    Top000 = 0,
    Top090,
    Top180,
    Top270,

    Bottom000,
    Bottom090,
    Bottom180,
    Bottom270,

    Forward000,
    Forward090,
    Forward180,
    Forward270,

    Back000,
    Back090,
    Back180,
    Back270,

    Right000,
    Right090,
    Right180,
    Right270,

    Left000,
    Left090,
    Left180,
    Left270,

    Max
}

## 動作確認

回転対応ができたので random に CubeRotationType を ChunkData に与えて、結果のメッシュを確認します。
余計な面が大きなキューブの内側に入っていなければ OK

結果はどうか?

残念 回転タイプ 0~7 までは問題なかったが
8から問題あり

11 まで修正(180度回転ずらせばよかった)

15 まではすでに正しかった。
16は、こちらも不具合あったが 180度回転ずらして修正を確認

19までOK
20から、すでに正しかった。

これでもう一度ランダムに回転を与えて結果を確認します。

f:id:simplestar_tech:20190616223015p:plain
正しく設定できました。

上記コードの方、正しいものに修正しておきました。

動くサンプルはこちら
github.com


続き
simplestar-tech.hatenablog.com

Unity:ポインターのポインターを入力にECS

# まえがき

Unity でキューブを敷き詰めた世界で、VRMオンラインチャットできることを目指して仕事の合間に技術調査してます。(かれこれ2年くらい)
Unity も 2017.1 から気づけば 2019.1 ですね。

キューブ一つひとつをメッシュオブジェクトにすると、下記を参考にわずか 20 x 20 x 20 個のキューブをシーンに配置するだけで、描画レートは 20 fps を下回ります。
Unity に限った話ではありませんが、参考までにこちら2年前の記事ですが…
https://cdn-ak.f.st-hatena.com/images/fotolife/s/simplestar_tech/20171009/20171009170254.jpg
六角柱を敷き詰めたマイクロワールドの構築(メッシュの結合とマルチマテリアル処理) - simplestarの技術ブログ

当時はメッシュを結合すれば爆速になることを確認して、とにかくメッシュを結合することを決めました。
https://cdn-ak.f.st-hatena.com/images/fotolife/s/simplestar_tech/20171016/20171016231940.jpg
結合してしまえば 215 x 215 x 215 個のキューブをシーンに配置しても 1087 fps 出た。

一旦その方向で技術調査をすすめていきましたが…
https://cdn-ak.f.st-hatena.com/images/fotolife/s/simplestar_tech/20180114/20180114104815.jpg

キューブまたは構成要素一つひとつのロジック計算量の見積もりが甘く…人間が面白いと感じる速度で世界は変化していかないわけです…
https://cdn-ak.f.st-hatena.com/images/fotolife/s/simplestar_tech/20180219/20180219001525.jpg
そこで目をつけたのが PC のメニーコア化と Unity ECS です。

(画像はイメージです)
https://cdn-ak.f.st-hatena.com/images/fotolife/s/simplestar_tech/20180204/20180204231816.gif

モンスター計算マシンと ECS なら、面白いと感じる速度で世界が変化するかも…と、そこで結果を出すために技術調査を続ける毎日です。

話を元に戻しますが、私達の生きているこの世界に果てはないそうで、地平線の向こう側まで直進し続けると元の場所に戻ってくるそうじゃないですか。
宇宙すら、不可能とされる光速を越えて直進し続けると出発点に戻ってくるとか

そこで 1 byte の 255 + 1 = 0 のオーバーフローを利用して、世界を 256 x 256 x 256 結合メッシュとする世界を考えます。

そろそろ本題ですが、結合メッシュの世界の最小構成要素(キューブ)配列を 256 x 256 x 256 (16,777,216) 要素の配列にして表現したく…
プログラマーの手段でこれを扱うと int** とポインターポインターになるわけです。(キューブの情報を int とするとね)
ECS はその原理からマネージド配列の操作を許しませんので(NativeArray または ポインター)、どうしてもこのポインターポインターを渡して仕事をさせたい訳なのです。

実はもうできそうなことわかっているので、その動作確認というものをこの記事で行ってみたいと思います。
できなかったらこの記事は完成しません。(恥ずかしい…)

# ECS によるメッシュ生成でキューブの配置

六角を廃して、最近作り直したキューブの世界…
https://cdn-ak.f.st-hatena.com/images/fotolife/s/simplestar_tech/20190321/20190321221355.jpg
動作確認できてよかったので、コードを移植していきます。
キューブ一つに int 32 bit が割り振られており
byte 8 bit ごとに
1.キューブカテゴリ
2.キューブ回転
3.A面タイプ
4.B面タイプ
の合計4つの意味を持つ値が入っています。
例えばキューブ回転はこんな感じ、値は時計回りの回転角度

public enum CubeRotation : byte
{
    Top000 = 0,
    Top090,
    Top180,
    Top270,

    Bottom000,
    Bottom090,
    Bottom180,
    Bottom270,

回転角度の適用は下の図の通りです。

f:id:simplestar_tech:20190614064059p:plain
左から Top000, 090, 180, 270
f:id:simplestar_tech:20190614063744p:plain
手前から Top 行、Bottom 行
f:id:simplestar_tech:20190614063651p:plain
そのほかの回転をわかりやすくする角度

関連ツィートはこちらから

このとき手打ちした頂点位置配列がこちら

        new Vector3[]{
            // Top
            new Vector3(-halfSide, +halfSide, +halfSide),
            new Vector3 (+halfSide, +halfSide, +halfSide),
            new Vector3 (+halfSide, +halfSide, -halfSide),
            // Bottom
            new Vector3 (+halfSide, -halfSide, -halfSide),
            new Vector3 (+halfSide, -halfSide, +halfSide),
            new Vector3 (-halfSide, -halfSide, +halfSide),
            // Right up
            new Vector3 (+halfSide, +halfSide, -halfSide),
            new Vector3 (+halfSide, +halfSide, +halfSide),
            new Vector3 (+halfSide, -halfSide, +halfSide),
            // Right down
            new Vector3 (+halfSide, -halfSide, +halfSide),
            new Vector3 (+halfSide, -halfSide, -halfSide),
            new Vector3 (+halfSide, +halfSide, -halfSide),
            // Forward up
            new Vector3 (+halfSide, +halfSide, +halfSide),
            new Vector3 (-halfSide, +halfSide, +halfSide),
            new Vector3 (-halfSide, -halfSide, +halfSide),
            // Forward down
            new Vector3 (-halfSide, -halfSide, +halfSide),
            new Vector3 (+halfSide, -halfSide, +halfSide),
            new Vector3 (+halfSide, +halfSide, +halfSide),
            // Cross up
            new Vector3 (-halfSide, +halfSide, +halfSide),
            new Vector3 (+halfSide, +halfSide, -halfSide),
            new Vector3 (+halfSide, -halfSide, -halfSide),
            // Cross down
            new Vector3 (+halfSide, -halfSide, -halfSide),
            new Vector3 (-halfSide, -halfSide, +halfSide),
            new Vector3 (-halfSide, +halfSide, +halfSide),

キューブを敷き詰めるときに重要なのが、周囲のキューブの情報の取得です。
処理を結合メッシュチャンクごとに分けた以上、チャンクをまたぐインデックスアクセスだと範囲外エラーとなります。

実はこの問題を解決するために、ポインターポインターが必要になります。

このまま解説のみで形にすることは難しいので、一度無回転のキューブだけを敷き詰め
周囲のチャンクの情報を取得しようとして困った状況というものを作ってみます。

頂点情報は上で作ったもので交換して、三角柱をキューブにして確認していきましょう。

f:id:simplestar_tech:20190614075221p:plain
キューブ化

頂点情報はこちらに交換しました。

        #region モデルの形状を定義→NativeArray確保
        // var vertices = BinaryUtility.Deserialize<float[]>(System.IO.Path.Combine(Application.dataPath, "Project/Resources/vertices.bytes"));
        const float halfSide = ChunkManager.CubeSide / 2.0f;
        var vertices = new float[]{
            // A面
            -halfSide, +halfSide, +halfSide,
            +halfSide, +halfSide, +halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, +halfSide,
            +halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, +halfSide,
            +halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            // B面
            -halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            -halfSide, +halfSide, -halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, -halfSide,
            -halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, -halfSide,
            -halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
        };
        this.pVerticesSource = (float*)(this.nativeVerticesSource = new NativeArray<float>(vertices, Allocator.Persistent)).GetUnsafePtr();
        #endregion

面と頂点数はとても勿体ない状況であることを確認します。

f:id:simplestar_tech:20190614075600p:plain
8x8x8のキューブメッシュ結合体 この頂点数は多すぎ!

メッシュ用の頂点数をカウントする、頂点情報を埋める処理において、周囲のキューブ情報を参照して、描画されることのない面を削る処理を追加してみます。

作る前に頭で想像すると
1.面ごとに頂点カウント、頂点情報作成
2.面ごとに描画対象かどうか
3.面ごとに、面の法線方向のキューブの値の取得、判定
4.汎用的に、キューブからの相対位置のキューブの値の取得
5.面の法線方向のキューブの相対位置の計算
が浮かびました。

1.キューブは切断面含め、三角形が 16 面あります。
頂点数はその3倍の48頂点
現在は 48 x 4 byte の MemCpy を一括で行っています。
これが 16 個に小分けになることになります。

つまりはこれが

            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                        var data = meshData.pChunkData[dataIndex];
                        if (1 == data)
                        {
                            var verticesOffset = dataIndex * this.sourceCount;
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset, this.pVerticesSource, size: this.sourceCount * sizeof(float));

                            for (int vertexIndex = 0; vertexIndex < this.sourceCount; vertexIndex+=3)
                            {
                                var positionXIndex = verticesOffset + vertexIndex;
                                meshData.pVerticesVector3[positionXIndex + 0] += x * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 1] += y * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 2] += z * ChunkManager.CubeSide;
                            }
                        }
                    }
                }
            }

こうってことです。

            var verticesOffset = 0;
            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                        var data = meshData.pChunkData[dataIndex];
                        if (1 == data)
                        {
                            var faceCount = 0;
                            // TopFaceA
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float));
                            faceCount++;
                            // BottomFaceA
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float));
                            faceCount++;
                            // SideFaceA1
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float) * 2);
                            faceCount += 2;
                            // SideFaceA2
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float) * 2);
                            faceCount += 2;
                            // CrossFaceA
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float) * 2);
                            faceCount += 2;

                            // TopFaceB
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float));
                            faceCount++;
                            // BottomFaceB
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float));
                            faceCount++;
                            // SideFaceB1
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float) * 2);
                            faceCount += 2;
                            // SideFaceB2
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float) * 2);
                            faceCount += 2;
                            // CrossFaceB
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float) * 2);
                            faceCount += 2;


                            var vector3Count = faceCount * 3;
                            var floatCount = vector3Count * 3;

                            for (int vertexIndex = 0; vertexIndex < floatCount; vertexIndex += 3)
                            {
                                var positionXIndex = verticesOffset + vertexIndex;
                                meshData.pVerticesVector3[positionXIndex + 0] += x * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 1] += y * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 2] += z * ChunkManager.CubeSide;
                            }
                            verticesOffset += floatCount;
                        }
                    }
                }
            }
        }

2.上記の処理を書いた後 16 面ごとに描画対象なのか判定を行うことになります。
yes/no を返す関数化しておきたいですが、引数はキューブ位置、キューブの情報、面の向きで解決できそうです。
3.面ごとに、面の法線方向のキューブの値の取得、判定
4.汎用的に、キューブからの相対位置のキューブの値の取得
5.面の法線方向のキューブの相対位置の計算
これらもクリアするロジックがこちら

                            // TopFaceA
                            var sideDataIndex = this.GetDataIndex(dataIndex, 0, 1, 0);
                            var sideData = this.GetData(meshData, sideDataIndex);


        /// <summary>
        /// データインデックスから相対的な座標のデータインデックスを取得
        /// </summary>
        int GetDataIndex(int dataIndex, int x, int y, int z)
        {
            dataIndex += x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY;
            dataIndex += z * ChunkManager.ChunkSizeY;
            dataIndex += y;
            return dataIndex;
        }

        int GetData(ChunkMeshData meshData, int dataIndex)
        {
            if (0 <= dataIndex && meshData.chunkDataLength > dataIndex)
            {
                return meshData.pChunkData[dataIndex];
            }
            else
            {
                // 困った
                return 0;
            }
        }

ここで、上記コメントの通り困ることに気が付きます。
GetDataIndex で得られる値が pChunkData の範囲外となった場合です。

ひとまず困った状態のまま、面どなりのデータを参照して、描く必要のない面をカリングしていきます。
ロジックはこんな感じ

                    // TopFaceB
                    {
                        var sideDataIndex = GetDataIndex(dataIndex, 0, 1, 0); // 一個上を見る
                        var pSideData = GetDataPtr(meshData, sideDataIndex);
                        if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                        {
                            // UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + faceCount * 9, size: 9 * sizeof(float));
                            faceCount++;
                        }
                    }

16面全部に適用してみるとひとまずはカリングされます。

f:id:simplestar_tech:20190615144955p:plain
16面を上記関数でカリングした結果
この結果を正しいと確信できる人は、私と同じこと考えているに違いない

# ポインターポインターで世界を 8 byte アドレスに
今回は 8 x 8 x 8 要素のチャンクを作り、これを使い回す形で 8 x8x8 個のエンティティを作成しています。
チャンクにインデックスを振るなら byte にする

現在はエンティティを個別に識別するための要素は Vector3 の位置だけ
その x, y, z 要素はそれぞれ Chunk の SideX, Y, Z の整数倍となっているので、単純に割ってから 256 で余りを得るとインデックスになる
ただ、ランタイムで割り算は行いたくないので、ここはエンティティ作成時に byte index を設定することにする

まずは byte インデックスの立体行列(テンソル)を作成して、開放するコードを書きます。

    unsafe void Start()
    {
        #region チャンクテンソルの作成
        this.ppChunkData= (int**)(UnsafeUtility.Malloc(sizeof(int*) * (byte.MaxValue + 1) * (byte.MaxValue + 1) * (byte.MaxValue + 1), sizeof(int*), Allocator.Persistent));
        #endregion



    void OnDestroy()
    {
        #region チャンクテンソルの破棄
        UnsafeUtility.Free((void*)this.ppChunkData, Allocator.Persistent);
        #endregion

チャンク(=エンティティ)インデックスの値はこのように決めることにしました。

        int chunkIndex = entityKey.x * (byte.MaxValue + 1) * (byte.MaxValue + 1) + entityKey.y * (byte.MaxValue + 1) + entityKey.z;
        this.ppChunkData[chunkIndex] = (int*)this.nativeChunkData.GetUnsafePtr();

ひとまず小ゴールとして ポインターポインターで世界を 8 byte アドレスにして動作確認できました。

    static byte* GetDataPtr(ChunkMeshData meshData, int dataIndex)
    {
        if (0 <= dataIndex && meshData.chunkDataLength > dataIndex)
        {
            var pChunkData = meshData.ppChunkData[meshData.chunkIndex];
            return (byte*)(pChunkData + dataIndex);
        }
        else
        {
            int* pData = meshData.ppChunkData[meshData.chunkIndex]; // @TODO どうやって周囲アクセス!?
            // 困った
            return null;
        }
    }

f:id:simplestar_tech:20190615175846p:plain
上記コードの実行時の様子

# 周囲の結合メッシュの情報を参照

周囲の結合メッシュ…つまりは entity です。
ただ、entity から entity にアクセスするのではなく、ポインターポインターの 8byte で世界の構成要素の情報を取得、書き換えできるようにここまで準備してきました。

具体的には ppChunkData から必要なチャンクを特定し、そこから値を取り出すようにしましょう。

コードを書く前に検算ですが、3次元を一次元配列にしている現在
x 方向に一つ足すと (byte.MaxValue + 1) * (byte.MaxValue + 1) 足すことを意味する
困ったことに x 方向に一つ足すべきなのかを判別する計算がわからない

今は chunkData 内の index でしかなく、もし境界の要素だったとして、そこから一つ足した場合でも chunkData 内に納まるインデックスだったら?
ね?
判別不可能でしょう。
そこで、chunkData 内の index を受け取るのをやめて、もっと情報量のボリュームがある三次元 index x, y, z を受け取るようにするのはどうか?
こうすると例えば y を一つたして chunk の y 最大を n 回オーバーするときに ppChunk 内でいくつ移動すればよいかがわかる
これにしましょう。
リファクタリングします。

f:id:simplestar_tech:20190615192908p:plain
リファクタリング後 まだチャンクをまたぐ参照ができていないカリング状態

このときはまだこのように、チャンクをまたぐ部分で困った状態となっています。

    static byte* GetDataPtr(ChunkMeshData meshData, int x, int y, int z)
    {
        if (0 <= x && ChunkManager.ChunkSizeX > x
            && 0 <= y && ChunkManager.ChunkSizeY > y
            && 0 <= z && ChunkManager.ChunkSizeZ > z)
        {
            var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
            var pChunkData = meshData.ppChunkData[meshData.chunkIndex];
            return (byte*)(pChunkData + dataIndex);
        }

        // over chunk count x, y, z
        // else
        {
            // 困った
            return null;
        }
    }

コメントにあるとおり、範囲外となったときに何個チャンクを跨いでいるのかを計算し、その分のチャンク移動を行ってみます。

コード修正中の様子がこちら、うすうす勘付いていたのですが…

    static byte* GetDataPtr(ChunkMeshData meshData, int x, int y, int z)
    {
        if (0 <= x && ChunkManager.ChunkSizeX > x
            && 0 <= y && ChunkManager.ChunkSizeY > y
            && 0 <= z && ChunkManager.ChunkSizeZ > z)
        {
            var pChunkData = meshData.ppChunkData[meshData.chunkIndex];
            var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
            return (byte*)(pChunkData + dataIndex);
        }

        var overChunkCountX = x / ChunkManager.ChunkSizeX;
        if (0 > x)
        {
            overChunkCountX -= 1;
        }
        x -= overChunkCountX * ChunkManager.ChunkSizeX;
        var overChunkCountY = y / ChunkManager.ChunkSizeY;
        if (0 > y)
        {
            overChunkCountY -= 1;
        }
        y -= overChunkCountY * ChunkManager.ChunkSizeY;
        var overChunkCountZ = z / ChunkManager.ChunkSizeZ;
        if (0 > z)
        {
            overChunkCountZ -= 1;
        }
        z -= overChunkCountZ * ChunkManager.ChunkSizeZ;

        // over chunk count x, y, z
        // else
        {
            // var chunkIndex = meshData.chunkIndex + 
            var pChunkData = meshData.ppChunkData[meshData.chunkIndex];
            var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
            return (byte*)(pChunkData + dataIndex);
        }
    }

やっぱり chunkIndex を計算する際、チャンクのインデックス x, y, z が必要です。
これをエンティティから取り出せるように修正します。

修正後

        #region 位置情報からチャンクキーに変換
        var byteMax = (byte.MaxValue + 1);
        var chunkKeyX = chunkPosition.x % byteMax;
        if (0 > chunkKeyX) chunkKeyX += byteMax;
        var chunkKeyY = chunkPosition.y % byteMax;
        if (0 > chunkKeyY) chunkKeyY += byteMax;
        var chunkKeyZ = chunkPosition.z % byteMax;
        if (0 > chunkKeyZ) chunkKeyZ += byteMax;
        #endregion

        #region チャンク情報の設定
        int chunkIndex = chunkKeyX * byteMax * byteMax + chunkKeyZ * byteMax + chunkKeyY;
        this.ppChunkData[chunkIndex] = (int*)this.nativeChunkData.GetUnsafePtr();
        meshEntity.EntityManager.SetComponentData(meshEntity.Entity, new ChunkMeshData
        {
            chunkKeyX = (byte)chunkKeyX,
            chunkKeyY = (byte)chunkKeyY,
            chunkKeyZ = (byte)chunkKeyZ,
            ppChunkData = this.ppChunkData,
        });
        #endregion

現在の meshData から x, y, z を指定してチャンクデータを取得するコード、修正後がこちら

    static byte* GetDataPtr(ChunkMeshData meshData, int x, int y, int z)
    {
        if (0 <= x && ChunkManager.ChunkSizeX > x
            && 0 <= y && ChunkManager.ChunkSizeY > y
            && 0 <= z && ChunkManager.ChunkSizeZ > z)
        {
            #region チャンク内のデータを返す
            var byteMax = (byte.MaxValue + 1);
            int chunkIndex = meshData.chunkKeyX * byteMax * byteMax + meshData.chunkKeyZ * byteMax + meshData.chunkKeyY;
            var pChunkData = meshData.ppChunkData[chunkIndex];
            var dataIndex = x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y;
            return (byte*)(pChunkData + dataIndex);
            #endregion
        }
        else
        {
            #region チャンク越えカウント
            var overChunkCountX = x / ChunkManager.ChunkSizeX;
            if (0 > x) overChunkCountX -= 1;
            var overChunkCountY = y / ChunkManager.ChunkSizeY;
            if (0 > y) overChunkCountY -= 1;
            var overChunkCountZ = z / ChunkManager.ChunkSizeZ;
            if (0 > z) overChunkCountZ -= 1;
            #endregion
            #region byte オーバーフローによる値ループ
            var chunkKeyX = (byte)(meshData.chunkKeyX + overChunkCountX);
            var chunkKeyY = (byte)(meshData.chunkKeyY + overChunkCountY);
            var chunkKeyZ = (byte)(meshData.chunkKeyZ + overChunkCountZ);
            #endregion
            #region チャンクの特定
            var byteMax = (byte.MaxValue + 1);
            int chunkIndex = chunkKeyX * byteMax * byteMax + chunkKeyZ * byteMax + chunkKeyY;
            var pChunkData = meshData.ppChunkData[chunkIndex];
            #endregion
            if (null != pChunkData)
            {
                #region チャンク内のデータインデックスへ変換
                x -= overChunkCountX * ChunkManager.ChunkSizeX;
                y -= overChunkCountY * ChunkManager.ChunkSizeY;
                z -= overChunkCountZ * ChunkManager.ChunkSizeZ;
                var dataIndex = x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y;
                #endregion
                return (byte*)(pChunkData + dataIndex);
            }

        }
        return null;
    }

一見して成功したかと思ったけど、X 方向のチャンク参照がすべて失敗している様子

f:id:simplestar_tech:20190615234026p:plain
不具合が見た目から確認した様子…期待ではボックスの内側はすべて空であるべき

動作確認していきます。

# 動作確認

overChunkCountX が 1 になるときの処理を追ってみます。
あれ、本来ポインターポインターに設定されているべきインデックスの値が空…

なるほど、そういえば前回の記事で前後のフレームに計算を分散させてましたね。
simplestar-tech.hatenablog.com
すっかり忘れてた…

以下の通り、全初期化を最初のフレームで行うことで解決!

    IEnumerator OnStartOfFrame()
    {
        while (true)
        {
            yield return null;
            this.startFrame = Time.realtimeSinceStartup;

            #region 作成対象のエンティティを Enqueue
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
                {
                    for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                    {
                        for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                        {
                            this.EnqueueCreateEntity(x, z, y);
                        }
                    }
                }
            }
            #endregion

            #region Queue から位置を取り出してMeshObjectをInstantiate
            this.createMeshObjectCount = 0;
            for (int dequeueIndex = 0; dequeueIndex < this.dequeueCount; dequeueIndex++)
            {
                if (0 == this.createEntityQueue.Count) {
                    break;
                }
                this.CreateChunkObjectEntity(this.createEntityQueue.Dequeue());
                this.createMeshObjectCount = dequeueIndex + 1;
            }
            #endregion
        }
    }

    void EnqueueCreateEntity(int x, int z, int y)
    {
        #region ポインターのポインターにチャンクをセット
        var byteMax = (byte.MaxValue + 1);
        int chunkIndex = x * byteMax * byteMax + z * byteMax + y;
        this.ppChunkData[chunkIndex] = (int*)this.nativeChunkData.GetUnsafePtr(); // 本来なら Amazon S3 からダウンロード完了後にこれしてから Enqueue とか?
        #endregion

        #region キーとなる位置情報を Enqueue
        var chunkPosition = new Vector3Int(x, y, z);
        this.createEntityQueue.Enqueue(chunkPosition);
        #endregion
    }

周辺チャンクへのアクセスが正しく行えるようになり、メッシュも期待通り削れました。

f:id:simplestar_tech:20190616071217p:plain
8x8x8 の結合メッシュ、倍数にして頂点数と三角形数が 0.047 倍まで削減された!

動作している絵

# まとめ

キューブの結合メッシュの元となるチャンクデータは int 配列でしたが
そんなチャンクデータもまた3次元テンソルになってまして…これを Unity ECS に渡すにはポインターポインターしかない!
と思ったので、動くかなーと手を動かしてみた記録が上記です。

ちょっと試してみよう…という発想と作業内容が全然釣り合わない感じですが、正しく動きましたので
ゼロから作るより、こういう作業でこういう結果が得られるのかと、参考にしていただけたらと思います。

最後に具体的な手段を載せます。

肝心の Entity 作成側の MonoBehaviour と ComponentSystem クラスの実装です。
CreateMeshWithECS.cs

using System.Collections;
using System.Collections.Generic;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using UnityEngine;

public unsafe class CreateMeshWithECS : MonoBehaviour
{
    #region Assets
    [SerializeField] GameObject prefabMeshObject;
    [SerializeField] Shader meshShader;
    #endregion

    void Awake()
    {
        Application.targetFrameRate = 60;
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World.Active); // 明示的に ECS デフォルトワールド使用を宣言
    }

    unsafe void Start()
    {
        #region チャンクテンソルの作成
        this.ppChunkData = (int**)(UnsafeUtility.Malloc(sizeof(int*) * (byte.MaxValue + 1) * (byte.MaxValue + 1) * (byte.MaxValue + 1), sizeof(int*), Allocator.Persistent));
        #endregion

        #region チャンク情報の定義
        var chunkData = new int[ChunkManager.ChunkSizeX * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY];
        for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
        {
            for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
            {
                for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                {
                    var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                    chunkData[dataIndex] = 16843009;
                }
            }
        }
        this.nativeChunkData = new NativeArray<int>(chunkData, Allocator.Persistent);
        #endregion

        StartCoroutine(this.OnStartOfFrame());
        StartCoroutine(this.OnEndOfFrame());
    }

    /// <summary>
    /// チャンクエンティティを作成
    /// </summary>
    void CreateChunkObjectEntity(Vector3Int chunkPosition)
    {
        #region メッシュオブジェクト Entity の作成
        var position = new Vector3(ChunkManager.ChunkSizeX * chunkPosition.x, ChunkManager.ChunkSizeY * chunkPosition.y, ChunkManager.ChunkSizeZ * chunkPosition.z);
        var meshObject = Instantiate(this.prefabMeshObject, position, Quaternion.identity);
        var meshEntity = meshObject.GetComponent<GameObjectEntity>();
        meshEntity.EntityManager.AddComponent(meshEntity.Entity, ComponentType.ReadOnly<CreateMeshMarker>());
        meshEntity.EntityManager.AddComponent(meshEntity.Entity, ComponentType.ReadWrite<ChunkMeshData>());
        #endregion

        #region 位置からチャンクキーに変換
        var byteMax = (byte.MaxValue + 1);
        var chunkKeyX = chunkPosition.x % byteMax;
        if (0 > chunkKeyX) chunkKeyX += byteMax;
        var chunkKeyY = chunkPosition.y % byteMax;
        if (0 > chunkKeyY) chunkKeyY += byteMax;
        var chunkKeyZ = chunkPosition.z % byteMax;
        if (0 > chunkKeyZ) chunkKeyZ += byteMax;
        #endregion

        #region チャンクキーとポインターのポインターを設定
        meshEntity.EntityManager.SetComponentData(meshEntity.Entity, new ChunkMeshData
        {
            chunkKeyX = (byte)chunkKeyX,
            chunkKeyY = (byte)chunkKeyY,
            chunkKeyZ = (byte)chunkKeyZ,
            ppChunkData = this.ppChunkData,
        });
        #endregion

        #region マテリアルの設定
        var renderer = meshObject.GetComponent<MeshRenderer>();
        renderer.material = new Material(this.meshShader);
        #endregion
    }

    IEnumerator OnStartOfFrame()
    {
        while (true)
        {
            yield return null;
            this.startFrame = Time.realtimeSinceStartup;

            #region 作成対象のエンティティを Enqueue
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
                {
                    for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                    {
                        for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                        {
                            this.EnqueueCreateEntity(x, z, y);
                        }
                    }
                }
            }
            #endregion

            #region Queue から位置を取り出してMeshObjectをInstantiate
            this.createMeshObjectCount = 0;
            for (int dequeueIndex = 0; dequeueIndex < this.dequeueCount; dequeueIndex++)
            {
                if (0 == this.createEntityQueue.Count) {
                    break;
                }
                this.CreateChunkObjectEntity(this.createEntityQueue.Dequeue());
                this.createMeshObjectCount = dequeueIndex + 1;
            }
            #endregion
        }
    }

    void EnqueueCreateEntity(int x, int z, int y)
    {
        #region ポインターのポインターにチャンクをセット
        var byteMax = (byte.MaxValue + 1);
        int chunkIndex = x * byteMax * byteMax + z * byteMax + y;
        this.ppChunkData[chunkIndex] = (int*)this.nativeChunkData.GetUnsafePtr(); // 本来なら Amazon S3 からダウンロード完了後にこれしてから Enqueue とか?
        #endregion

        #region キーとなる位置情報を Enqueue
        var chunkPosition = new Vector3Int(x, y, z);
        this.createEntityQueue.Enqueue(chunkPosition);
        #endregion
    }

    IEnumerator OnEndOfFrame()
    {
        while (true)
        {
            yield return new WaitForEndOfFrame();
            this.endFrame = Time.realtimeSinceStartup;

            #region 処理時間を見て次のフレームの dequeueCount を決定
            if (0 < this.createMeshObjectCount)
            {
                var updateTime = (this.endFrame - this.startFrame);
                var entityTime = updateTime / this.createMeshObjectCount;
                var targetFrameSecond = 1f / Application.targetFrameRate;
                this.dequeueCount = Mathf.FloorToInt(Mathf.Lerp(this.dequeueCount, targetFrameSecond / entityTime, 0.7f));
            }
            if (0 == this.dequeueCount)
            {
                this.dequeueCount = 1;
            }
            #endregion
        }
    }

    void OnDestroy()
    {
        #region メッシュ情報破棄
        this.nativeChunkData.Dispose();
        #endregion
        #region チャンクテンソルの破棄
        UnsafeUtility.Free((void*)this.ppChunkData, Allocator.Persistent);
        #endregion
        #region ECS 終了処理
        World.DisposeAllWorlds();
        WordStorage.Instance.Dispose();
        WordStorage.Instance = null;
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(null);
        #endregion
    }

    NativeArray<int> nativeChunkData;
    float startFrame;
    float endFrame;
    Queue<Vector3Int> createEntityQueue = new Queue<Vector3Int>();
    int dequeueCount = 1;
    int createMeshObjectCount = 0;
    int** ppChunkData = null;
}

CreateMeshSystem.cs

using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;

internal unsafe class CreateMeshSystem : ComponentSystem
{
    /// <summary>
    /// チャンク情報から頂点数をカウント
    /// </summary>
    [BurstCompile]
    unsafe struct CountVerticesJob : IJobParallelFor
    {
        internal NativeArray<ChunkMeshData> meshDataArray;
        internal int sourceCount;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            meshData.vertexCount = 0;
            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var pData = GetDataPtr(meshData, x, y, z);
                        if (1 == pData[0])
                        {
                            var faceCount = 0;
                            // TopFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x, y + 1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    faceCount++;
                                }
                            }
                            // BottomFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x, y - 1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    faceCount++;
                                }
                            }
                            // RightSideFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x + 1, y, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    faceCount += 2;
                                }
                            }
                            // ForwardSideFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x, y, z + 1);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    faceCount += 2;
                                }
                            }
                            // CrossFaceA
                            {
                                if (!(1 == pData[3]))
                                {
                                    faceCount += 2;
                                }
                            }

                            // BottomFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x, y - 1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    faceCount++;
                                }
                            }
                            // TopFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x, y + 1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    faceCount++;
                                }
                            }
                            // LeftSideFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x - 1, y, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    faceCount += 2;
                                }
                            }
                            // BackSideFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x, y, z - 1);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    faceCount += 2;
                                }
                            }
                            // CrossFaceB
                            {
                                if (!(1 == pData[2]))
                                {
                                    faceCount += 2;
                                }
                            }

                            var vertexCount = faceCount * 3;
                            var floatCount = vertexCount * 3;
                            meshData.vertexCount += floatCount;
                        }
                    }
                }
            }
            this.meshDataArray[entityIndex] = meshData;
        }
    }

    /// <summary>
    /// 頂点バッファに頂点データをコピー書き込み
    /// </summary>
    [BurstCompile]
    unsafe struct CopyWriteVerticesJob : IJobParallelFor
    {
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pVerticesSource;
        [ReadOnly] internal int sourceCount;
        internal NativeArray<ChunkMeshData> meshDataArray;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            var verticesOffset = 0;
            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var pData = GetDataPtr(meshData, x, y, z);
                        if (1 == pData[0])
                        {
                            var faceCount = 0;
                            // TopFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x, y + 1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 0 * 9, size: 9 * sizeof(float));
                                    faceCount++;
                                }
                            }
                            // BottomFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x, y -1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 1 * 9, size: 9 * sizeof(float));
                                    faceCount++;
                                }
                            }
                            // RightSideFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x + 1, y, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 2 * 9, size: 9 * sizeof(float) * 2);
                                    faceCount += 2;
                                }
                            }
                            // ForwardSideFaceA
                            {
                                var pSideData = GetDataPtr(meshData, x, y, z + 1);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[2]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 4 * 9, size: 9 * sizeof(float) * 2);
                                    faceCount += 2;
                                }
                            }
                            // CrossFaceA
                            {
                                if (!(1 == pData[3]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 6 * 9, size: 9 * sizeof(float) * 2);
                                    faceCount += 2;
                                }
                            }

                            // BottomFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x, y - 1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 8 * 9, size: 9 * sizeof(float));
                                    faceCount++;
                                }
                            }
                            // TopFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x, y + 1, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 9 * 9, size: 9 * sizeof(float));
                                    faceCount++;
                                }
                            }
                            // LeftSideFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x - 1, y, z);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 10 * 9, size: 9 * sizeof(float) * 2);
                                    faceCount += 2;
                                }
                            }
                            // BackSideFaceB
                            {
                                var pSideData = GetDataPtr(meshData, x, y, z - 1);
                                if (!(null != pSideData && 1 == pSideData[0] && 1 == pSideData[1] && 1 == pSideData[3]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 12 * 9, size: 9 * sizeof(float) * 2);
                                    faceCount += 2;
                                }
                            }
                            // CrossFaceB
                            {
                                if (!(1 == pData[2]))
                                {
                                    UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset + faceCount * 9, this.pVerticesSource + 14 * 9, size: 9 * sizeof(float) * 2);
                                    faceCount += 2;
                                }
                            }

                            var vertexCount = faceCount * 3;
                            var floatCount = vertexCount * 3;

                            for (int vertexIndex = 0; vertexIndex < floatCount; vertexIndex += 3)
                            {
                                var positionXIndex = verticesOffset + vertexIndex;
                                meshData.pVerticesVector3[positionXIndex + 0] += x * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 1] += y * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 2] += z * ChunkManager.CubeSide;
                            }
                            verticesOffset += floatCount;
                        }
                    }
                }
            }
        }
    }

    static byte* GetDataPtr(ChunkMeshData meshData, int x, int y, int z)
    {
        if (0 <= x && ChunkManager.ChunkSizeX > x
            && 0 <= y && ChunkManager.ChunkSizeY > y
            && 0 <= z && ChunkManager.ChunkSizeZ > z)
        {
            #region チャンク内のデータを返す
            var byteMax = (byte.MaxValue + 1);
            int chunkIndex = meshData.chunkKeyX * byteMax * byteMax + meshData.chunkKeyZ * byteMax + meshData.chunkKeyY;
            var pChunkData = meshData.ppChunkData[chunkIndex];
            var dataIndex = x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y;
            return (byte*)(pChunkData + dataIndex);
            #endregion
        }
        else
        {
            #region チャンク越えカウント
            var overChunkCountX = x / ChunkManager.ChunkSizeX;
            if (0 > x) overChunkCountX -= 1;
            var overChunkCountY = y / ChunkManager.ChunkSizeY;
            if (0 > y) overChunkCountY -= 1;
            var overChunkCountZ = z / ChunkManager.ChunkSizeZ;
            if (0 > z) overChunkCountZ -= 1;
            #endregion
            #region byte オーバーフローによる値ループ
            var chunkKeyX = (byte)(meshData.chunkKeyX + overChunkCountX);
            var chunkKeyY = (byte)(meshData.chunkKeyY + overChunkCountY);
            var chunkKeyZ = (byte)(meshData.chunkKeyZ + overChunkCountZ);
            #endregion
            #region チャンクの特定
            var byteMax = (byte.MaxValue + 1);
            int chunkIndex = chunkKeyX * byteMax * byteMax + chunkKeyZ * byteMax + chunkKeyY;
            var pChunkData = meshData.ppChunkData[chunkIndex];
            #endregion
            if (null != pChunkData)
            {
                #region チャンク内のデータインデックスへ変換
                x -= overChunkCountX * ChunkManager.ChunkSizeX;
                y -= overChunkCountY * ChunkManager.ChunkSizeY;
                z -= overChunkCountZ * ChunkManager.ChunkSizeZ;
                var dataIndex = x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y;
                #endregion
                return (byte*)(pChunkData + dataIndex);
            }

        }
        return null;
    }

    protected override unsafe void OnCreate()
    {
        base.OnCreate();

        this.query = GetEntityQuery(new EntityQueryDesc
        {
            All = new[] { ComponentType.ReadOnly<CreateMeshMarker>(),
                ComponentType.ReadWrite<ChunkMeshData>() },
        });

        #region モデルの形状を定義→NativeArray確保
        // var vertices = BinaryUtility.Deserialize<float[]>(System.IO.Path.Combine(Application.dataPath, "Project/Resources/vertices.bytes"));
        const float halfSide = ChunkManager.CubeSide / 2.0f;
        var vertices = new float[]{
            // A面
            -halfSide, +halfSide, +halfSide,
            +halfSide, +halfSide, +halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, +halfSide,
            +halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, +halfSide,
            +halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            // B面
            -halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            -halfSide, +halfSide, -halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, -halfSide,
            -halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, -halfSide, -halfSide,
            -halfSide, -halfSide, -halfSide,
            -halfSide, +halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
            -halfSide, +halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            -halfSide, -halfSide, +halfSide,
            +halfSide, -halfSide, -halfSide,
            +halfSide, +halfSide, -halfSide,
        };
        this.pVerticesSource = (float*)(this.nativeVerticesSource = new NativeArray<float>(vertices, Allocator.Persistent)).GetUnsafePtr();
        #endregion
    }

    protected override void OnDestroy()
    {
        this.nativeVerticesSource.Dispose();

        base.OnDestroy();
    }

    protected unsafe override void OnUpdate()
    {
        #region NativeArray 確保
        var entities = this.query.ToEntityArray(Allocator.TempJob);
        var meshDataArray = this.query.ToComponentDataArray<ChunkMeshData>(Allocator.TempJob);
        #endregion

        #region メッシュの頂点数をカウント
        var countVerticesJob = new CountVerticesJob
        {
            sourceCount = this.nativeVerticesSource.Length,
            meshDataArray = meshDataArray
        };
        var countJobHandle = countVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        countJobHandle.Complete();
        #endregion

        #region カウント数→頂点バッファを確保→バッファポインタを ComponentData に代入
        this.entityMeshDataList.Clear();
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var meshData = meshDataArray[entityIndex];
            var countVector3 = meshData.vertexCount / 3;
            var entityMeshData = new EntityMeshData {
                nativeVertices = new NativeArray<Vector3>(countVector3, Allocator.TempJob),
            };
            this.entityMeshDataList.Add(entityMeshData);
            meshData.pVerticesVector3 = (float*)entityMeshData.nativeVertices.GetUnsafePtr();
            meshDataArray[entityIndex] = meshData;
        }
        #endregion

        #region 頂点バッファに頂点データをコピー
        var copyVerticesJob = new CopyWriteVerticesJob
        {
            pVerticesSource = this.pVerticesSource,
            sourceCount = this.nativeVerticesSource.Length,
            meshDataArray = meshDataArray
        };
        var copyJobHandle = copyVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        copyJobHandle.Complete();
        #endregion

        #region 頂点バッファ→マネージド配列→メッシュ作成→メッシュ法線・接線の計算
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            var entityMeshData = this.entityMeshDataList[entityIndex];
            var vertices = entityMeshData.nativeVertices.ToArray();

            var mesh = new Mesh();
            mesh.Clear();
            mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
            mesh.vertices = vertices;
            int[] triangles = new int[vertices.Length];
            for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
            {
                triangles[vertexIndex] = vertexIndex;
            }
            mesh.SetIndices(triangles, MeshTopology.Triangles, submesh: 0, calculateBounds: true);
            mesh.RecalculateNormals();
            mesh.RecalculateTangents();
            var meshFilter = EntityManager.GetComponentObject<MeshFilter>(entity);
            meshFilter.mesh = mesh;

            entityMeshData.nativeVertices.Dispose();
        }
        this.entityMeshDataList.Clear();
        #endregion

        #region entity から marker の除去
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            EntityManager.RemoveComponent(entity, ComponentType.ReadOnly<CreateMeshMarker>());
        }
        #endregion

        #region NativeArray 開放
        meshDataArray.Dispose();
        entities.Dispose();
        #endregion
    }

    EntityQuery query;
    
    NativeArray<float> nativeVerticesSource;
    float* pVerticesSource;

    List<EntityMeshData> entityMeshDataList = new List<EntityMeshData>();

    class EntityMeshData
    {
        public NativeArray<Vector3> nativeVertices;
    }
}

最後まで読んでくれてありがとう!

動くサンプルはこちら
github.com


続きはこちら
simplestar-tech.hatenablog.com

Unity:ECSでメッシュオブジェクトを生成する方法

まえがき

Unity で動的にメッシュオブジェクトを作成して、キャラクターと衝突判定を行えるようにコライダーを設定する…となると?

Unity の書式により
1.シーン内に GameObject インスタンスを作り、追加コンポーネントは MeshFilter と Renderer, MeshCollider が必須となります。
2.Vector3や int, Vector2 のマネージド配列を vertices, indices(submeshごとに), uvs(submeshごとに) を作成し MeshFilter に設定、normals のために CalcurateNormals を行う必要があります。
3.その後、MeshCollider に mesh を渡してコライダーを再構築します。
4.マテリアルを設定します。(submeshごとに)

この内容であれば、過去に二回記事を書いてきましたし、そこからさらにプリミティブな技術に関する記事へ飛ぶことができます。
以下の通り。
simplestar-tech.hatenablog.com

今回は、さらに ECS を使ってステップ2のvertices や indices をマルチスレッドで作成する最小サンプルの動作確認をしていきます。

ECS についてわからない人はこちらを参照して理解してくる必要があります。
qiita.com
自分は上記の Tips 集を経て、コードが書けるようになってから、こちらの解説を読めるようになるのが一番簡単なルートかなと感じてます。
www.f-sp.com

Unity ECS の理解は難易度が高いです。
数ヶ月勉強しても、よくわからない状態が続いても、あきらめないことが大事です。
筆者も一ヶ月近くサンプルやドキュメント読みながら、よくわからない状態が続きましたが、思ったとおりに動いてしまえばこっちのものです。

そんなわけで、Unity ECS と動的メッシュ作成を見ていきましょう!

Unity のCPU利用順序

ゲーム開発は多次元のイメージに様々な角度からライトを当てて、一次元のコードとして影をはっきり描く、高い集中力と体力を必要とする知的活動です。
影として描く一次元のコードというのがミソで、かならず真っ直ぐなラインで順序を数えることができます。
まずは Unity でコードを書いたら、どういう順番で処理されるのかを正確に把握するところから始めます。
simplestar-tech.hatenablog.com

もう一つ重要なこととして、ユーザーさんは、画面がカクつく(描画タイミングが一瞬遅れる)ことに強いストレスを感じます。
メッシュは数百万頂点から構成されているので、どうしても計算リソースが描画レートを上回ります。
そのため、画面がカクつく前に計算量を調整する仕組みを用意しなければなりません。

その仕組みも今回のサンプルに盛り込んでみましょう。

まずは一つ ECS を使わずにメッシュを作る

ざっとおさらい
CreateMeshWithoutECS.cs

using UnityEngine;

/// <summary>
/// ECS を使わずに動的に Mesh 作成をする
/// </summary>
public class CreateMeshWithoutECS : MonoBehaviour
{
    #region Assets
    [SerializeField] GameObject prefabMeshObject;
    [SerializeField] Shader meshShader;
    #endregion

    const float root3 = 1.732051f;

    void Start()
    {
        var gameObject = Instantiate(this.prefabMeshObject);
        var mesh = new Mesh();
        Vector3[] positions = new Vector3[] {
            // top
            new Vector3 (0f, 1f, 0f),
            new Vector3 (1f, 1f, -root3),
            new Vector3 (-1f, 1f, -root3),
            // bottom
            new Vector3 (0f, 0f, 0f),
            new Vector3 (-1f, 0f, -root3),
            new Vector3 (1f, 0f, -root3),
        };
        int[] vertIndices = new int[]
        {
            0, 1, 2,
            3, 4, 5,
            0, 2, 4,
            4, 3, 0,
            2, 1, 5,
            5, 4, 2,
            1, 0, 3,
            3, 5, 1
        };
        Vector3[] vertices = new Vector3[vertIndices.Length];
        for (int i = 0; i < vertIndices.Length; i++)
        {
            vertices[i] = positions[vertIndices[i]];
        }
        mesh.vertices = vertices;

        int[] triangles = new int[mesh.vertices.Length];
        for (int i = 0; i < mesh.vertices.Length; i++)
        {
            triangles[i] = i;
        }
        mesh.triangles = triangles;

        mesh.RecalculateNormals();

        var filter = gameObject.GetComponent<MeshFilter>();
        filter.sharedMesh = mesh;

        var renderer = gameObject.GetComponent<MeshRenderer>();
        renderer.material = new Material(this.meshShader);
    }
}

結果よし

f:id:simplestar_tech:20190609102828p:plain
上記コードの実行結果

ECS でメッシュを作る

Hybrid ECS と呼ばれる GameObject に予め GameObjectEntity コンポーネントを設定する方法で攻めます。
次の MonoBehaviour をシーンに配置すると entity が一つ作成されます。
ここで書かれているチャンク情報は今回の記事では活躍しないので無視してください。

CreateMeshWithECS.cs

using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using UnityEngine;

public class CreateMeshWithECS : MonoBehaviour
{
    #region Assets
    [SerializeField] GameObject prefabMeshObject;
    [SerializeField] Shader meshShader;
    #endregion

    void Awake()
    {
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(World.Active); // 明示的に ECS デフォルトワールド使用を宣言
    }

    unsafe void Start()
    {
        #region メッシュオブジェクト Entity の作成
        var meshObject = Instantiate(this.prefabMeshObject);
        var meshEntity = meshObject.GetComponent<GameObjectEntity>();
        meshEntity.EntityManager.AddComponent(meshEntity.Entity, ComponentType.ReadOnly<CreateMeshMarker>());
        meshEntity.EntityManager.AddComponent(meshEntity.Entity, ComponentType.ReadWrite<ChunkMeshData>());
        #endregion
        #region チャンク情報の定義
        this.nativeChunkData = new NativeArray<int>(
            new int[] {
                1
            }, Allocator.Persistent);
        #endregion
        #region チャンク情報の設定
        meshEntity.EntityManager.SetComponentData<ChunkMeshData>(meshEntity.Entity, new ChunkMeshData
        {
            pChunkData = (int*)this.nativeChunkData.GetUnsafePtr(),
            chunkDataLength = this.nativeChunkData.Length
        });
        #endregion

        var renderer = meshObject.GetComponent<MeshRenderer>();
        renderer.material = new Material(this.meshShader);
    }

    void OnDestroy()
    {
        #region メッシュ情報破棄
        this.nativeChunkData.Dispose();
        #endregion
        #region ECS 終了処理
        World.DisposeAllWorlds();
        WordStorage.Instance.Dispose();
        WordStorage.Instance = null;
        ScriptBehaviourUpdateOrder.UpdatePlayerLoop(null);
        #endregion
    }

    NativeArray<int> nativeChunkData;
}

出てきた登場クラス紹介

CreateMeshComponents.cs

using Unity.Entities;

internal unsafe struct ChunkMeshData : IComponentData
{
    internal int* pChunkData;
    internal int chunkDataLength;
    internal float* pVerticesVector3;
    internal int vertexCount;   
}

MarkerInterfaces.cs

using Unity.Entities;
public struct CreateMeshMarker : IComponentData { }

次の ComponentSystem を実装しておけば、ECS が entity マーカーである CreateMeshMarker を持つ entity 配列に対して処理を実行するようになります。
初期化 OnCreate 関数内では頂点データのベースを定義し、OnDestroy 関数内で、確保した領域を開放しています。

並列ジョブを entity 単位で実行するように実装しました。
キューブどころか三角柱のメッシュデータを利用していますので、これ以上小さい ECS 動的メッシュ生成サンプルは作れない気がしています。
Job も頂点数カウントとバッファ書き込みの2つを直列で実行するように、一つの System で2つの Job を記述するようにまとめました。

CreateMeshSystem.cs

using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;

internal unsafe class CreateMeshSystem : ComponentSystem
{
    /// <summary>
    /// チャンク情報から頂点数をカウント
    /// </summary>
    [BurstCompile]    
    unsafe struct CountVerticesJob : IJobParallelFor
    {
        internal NativeArray<ChunkMeshData> meshDataArray;
        internal int verticesSourceCount;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            meshData.vertexCount = 0;
            for (int dataIndex = 0; dataIndex < meshData.chunkDataLength; dataIndex++)
            {
                var data = meshData.pChunkData[dataIndex];
                if (1 == data)
                {
                    meshData.vertexCount += this.verticesSourceCount;
                }
            }
            this.meshDataArray[entityIndex] = meshData;
        }
    }

    /// <summary>
    /// 頂点バッファに頂点データを塗る
    /// </summary>
    [BurstCompile] 
    unsafe struct FillVerticesJob : IJobParallelFor
    {
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pVerticesSourceVector3;

        internal NativeArray<ChunkMeshData> meshDataArray;
        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            for (int dataIndex = 0; dataIndex < meshData.chunkDataLength; dataIndex++)
            {
                var data = meshData.pChunkData[dataIndex];
                if (1 == data)
                {
                    UnsafeUtility.MemCpy(meshData.pVerticesVector3, this.pVerticesSourceVector3, size: meshData.vertexCount * 3 * sizeof(float));
                }
            }
        }
    }

    protected override unsafe void OnCreate()
    {
        base.OnCreate();

        this.query = GetEntityQuery(new EntityQueryDesc
        {
            All = new[] { ComponentType.ReadOnly<CreateMeshMarker>(),
                ComponentType.ReadWrite<ChunkMeshData>() },
        });

        #region モデルの形状を定義→NativeArray確保
        int[] indices = new int[]
        {
            0, 1, 2,
            3, 4, 5,
            0, 2, 4,
            4, 3, 0,
            2, 1, 5,
            5, 4, 2,
            1, 0, 3,
            3, 5, 1
        };
        const float root3 = 1.732051f;
        var vertices = new Vector3[] {
            new Vector3(0f, 1f, 0f),
            new Vector3(1f, 1f, -root3),
            new Vector3(-1f, 1f, -root3),
            new Vector3(0f, 0f, 0f),
            new Vector3(-1f, 0f, -root3),
            new Vector3(1f, 0f, -root3),
        };
        Vector3[] verticesSourceVector3 = new Vector3[indices.Length];
        for (int indicesIndex = 0; indicesIndex < indices.Length; indicesIndex++)
        {
            verticesSourceVector3[indicesIndex] = vertices[indices[indicesIndex]];
        }
        this.pVerticesSourceVector3 = (float*)(this.nativeVerticesSource = new NativeArray<Vector3>(verticesSourceVector3, Allocator.Persistent)).GetUnsafePtr();
        #endregion
    }

    protected override void OnDestroy()
    {
        this.nativeVerticesSource.Dispose(); // NativeArray 開放

        base.OnDestroy();
    }

    protected unsafe override void OnUpdate()
    {
        #region NativeArray 確保
        var entities = this.query.ToEntityArray(Allocator.TempJob);
        var meshDataArray = this.query.ToComponentDataArray<ChunkMeshData>(Allocator.TempJob);
        #endregion

        #region メッシュの頂点数をカウント
        var countVerticesJob = new CountVerticesJob
        {
            verticesSourceCount = this.nativeVerticesSource.Length,
            meshDataArray = meshDataArray
        };
        var jobHandle = countVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        jobHandle.Complete();
        #endregion

        #region カウント数→頂点バッファを確保→バッファポインタを ComponentData に代入
        this.nativeEntityVerticesList.Clear();
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var meshData = meshDataArray[entityIndex];
            var nativeVertices = new NativeArray<Vector3>(meshData.vertexCount, Allocator.TempJob);
            this.nativeEntityVerticesList.Add(nativeVertices);
            meshData.pVerticesVector3 = (float*)nativeVertices.GetUnsafePtr();
            meshDataArray[entityIndex] = meshData;
        }
        #endregion

        #region 頂点バッファに頂点データを塗る
        var fillVerticesJob = new FillVerticesJob
        {
            pVerticesSourceVector3 = this.pVerticesSourceVector3,
            meshDataArray = meshDataArray
        };
        var fillJobHandle = fillVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        fillJobHandle.Complete();
        #endregion

        #region 頂点バッファ→マネージド配列→メッシュ作成→メッシュ法線・接線の計算
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            var vertices = this.nativeEntityVerticesList[entityIndex].ToArray();

            var mesh = new Mesh();
            mesh.vertices = vertices;
            int[] triangles = new int[vertices.Length];
            for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
            {
                triangles[vertexIndex] = vertexIndex;
            }
            mesh.SetIndices(triangles, MeshTopology.Triangles, submesh: 0, calculateBounds: true);
            mesh.RecalculateNormals();
            mesh.RecalculateTangents();

            var meshFilter = EntityManager.GetComponentObject<MeshFilter>(entity);
            meshFilter.mesh = mesh;

            this.nativeEntityVerticesList[entityIndex].Dispose();
        }
        this.nativeEntityVerticesList.Clear();
        #endregion

        #region entity から marker の除去
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            EntityManager.RemoveComponent(entity, ComponentType.ReadOnly<CreateMeshMarker>());
        }
        #endregion

        #region NativeArray 開放
        meshDataArray.Dispose();
        entities.Dispose();
        #endregion
    }

    EntityQuery query;
    
    NativeArray<Vector3> nativeVerticesSource;
    float* pVerticesSourceVector3;

    List<NativeArray<Vector3>> nativeEntityVerticesList = new List<NativeArray<Vector3>>();
}

with ECS で同じ結果を得られることを確認しました。

f:id:simplestar_tech:20190609183536p:plain
上記サンプルコードを実行したときの様子

カクつかない仕組みを作る

まずはカクつかせるようにメッシュを複雑にして entity の数を増やしてみましょう。

上記のコードを次の通り変更します。

public class ChunkManager
{ 
    public const int ChunkSizeX = 16;
    public const int ChunkSizeY = 16;
    public const int ChunkSizeZ = 16;

    public const float CubeSide = 2f;
}

        #region チャンク情報の定義
        var chunkData = new int[ChunkManager.ChunkSizeX * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY];
        for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
        {
            for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
            {
                for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                {
                    var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                    chunkData[dataIndex] = 1;
                }
            }
        }
        this.nativeChunkData = new NativeArray<int>(chunkData, Allocator.Persistent);
        #endregion
        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];

            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                        var data = meshData.pChunkData[dataIndex];
                        if (1 == data)
                        {
                            var verticesOffset = dataIndex * this.verticesSourceCount;
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset * 3, this.pVerticesSourceVector3, size: this.verticesSourceCount * 3 * sizeof(float));

                            for (int vertexIndex = 0; vertexIndex < this.verticesSourceCount; vertexIndex++)
                            {
                                var positionXIndex = (verticesOffset + vertexIndex) * 3;
                                meshData.pVerticesVector3[positionXIndex + 0] += x * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 1] += y * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 2] += z * ChunkManager.CubeSide;
                            }
                        }
                    }
                }
            }
        }

f:id:simplestar_tech:20190609225400p:plain
16x16x16 のチャンクにしてみる

さらに entity を増やします。

コードはこんな変更をします。

    private unsafe void CreateMeshObject(Vector3 position)
    {
        #region メッシュオブジェクト Entity の作成
        var meshObject = Instantiate(this.prefabMeshObject, position, Quaternion.identity);
        var meshEntity = meshObject.GetComponent<GameObjectEntity>();
        meshEntity.EntityManager.AddComponent(meshEntity.Entity, ComponentType.ReadOnly<CreateMeshMarker>());
        meshEntity.EntityManager.AddComponent(meshEntity.Entity, ComponentType.ReadWrite<ChunkMeshData>());
        #endregion
        
        #region チャンク情報の設定
        meshEntity.EntityManager.SetComponentData<ChunkMeshData>(meshEntity.Entity, new ChunkMeshData
        {
            pChunkData = (int*)this.nativeChunkData.GetUnsafePtr(),
            chunkDataLength = this.nativeChunkData.Length
        });
        #endregion

        var renderer = meshObject.GetComponent<MeshRenderer>();
        renderer.material = new Material(this.meshShader);
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var position = new Vector3(ChunkManager.ChunkSizeX * x * 2, ChunkManager.ChunkSizeY * y, ChunkManager.ChunkSizeZ * z * 1.732f);
                        CreateMeshObject(position);
                    }
                }
            }
            
        }
    }

f:id:simplestar_tech:20190610213701p:plain
8x8x8 のチャンクを 8x8x8 個作成した様子

プロファイラを確認すると大変なスパイクが発生していることが確認できました。

f:id:simplestar_tech:20190610214042p:plain
スパイク時の Timeline (1132.64ms)

normal と tangent の計算に時間を要しているようです。
normal と tangent をあらかじめ与えてコピーするようにしてみたらどうなるでしょうか?

f:id:simplestar_tech:20190611212548p:plain
normals と tangents の計算をしなかった場合Timeline (1202.04ms)

若干遅くなってしまいました。参考までにソースコードを示します。

using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;

internal unsafe class CreateMeshSystem : ComponentSystem
{
    /// <summary>
    /// チャンク情報から頂点数をカウント
    /// </summary>
    [BurstCompile]
    unsafe struct CountVerticesJob : IJobParallelFor
    {
        internal NativeArray<ChunkMeshData> meshDataArray;
        internal int verticesSourceCount;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];
            meshData.vertexCount = 0;
            for (int dataIndex = 0; dataIndex < meshData.chunkDataLength; dataIndex++)
            {
                var data = meshData.pChunkData[dataIndex];
                if (1 == data)
                {
                    meshData.vertexCount += this.verticesSourceCount;
                }
            }
            this.meshDataArray[entityIndex] = meshData;
        }
    }

    /// <summary>
    /// 頂点バッファに頂点データを塗る
    /// </summary>
    [BurstCompile]
    unsafe struct FillVerticesJob : IJobParallelFor
    {
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pVerticesSource;
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pNormalsSource;
        [NativeDisableUnsafePtrRestriction] [ReadOnly] internal float* pTangentsSource;
        [ReadOnly] internal int sourceCount;
        internal NativeArray<ChunkMeshData> meshDataArray;

        public void Execute(int entityIndex)
        {
            var meshData = this.meshDataArray[entityIndex];

            for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
            {
                for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                {
                    for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                    {
                        var dataIndex = (x * ChunkManager.ChunkSizeZ * ChunkManager.ChunkSizeY + z * ChunkManager.ChunkSizeY + y);
                        var data = meshData.pChunkData[dataIndex];
                        if (1 == data)
                        {
                            var verticesOffset = dataIndex * this.sourceCount;
                            UnsafeUtility.MemCpy(meshData.pVerticesVector3 + verticesOffset, this.pVerticesSource, size: this.sourceCount * sizeof(float));
                            UnsafeUtility.MemCpy(meshData.pNormalsVector3 + verticesOffset, this.pNormalsSource, size: this.sourceCount * sizeof(float));
                            UnsafeUtility.MemCpy(meshData.pTangentsVector4 + verticesOffset * 4 / 3, this.pTangentsSource, size: this.sourceCount * 4 / 3 * sizeof(float));

                            for (int vertexIndex = 0; vertexIndex < this.sourceCount; vertexIndex+=3)
                            {
                                var positionXIndex = verticesOffset + vertexIndex;
                                meshData.pVerticesVector3[positionXIndex + 0] += x * ChunkManager.CubeSide;
                                meshData.pVerticesVector3[positionXIndex + 1] += y * ChunkManager.CubeSide / 2;
                                meshData.pVerticesVector3[positionXIndex + 2] += z * ChunkManager.CubeSide;
                            }
                        }
                    }
                }
            }
        }
    }

    protected override unsafe void OnCreate()
    {
        base.OnCreate();

        this.query = GetEntityQuery(new EntityQueryDesc
        {
            All = new[] { ComponentType.ReadOnly<CreateMeshMarker>(),
                ComponentType.ReadWrite<ChunkMeshData>() },
        });

        #region モデルの形状を定義→NativeArray確保
        var vertices = BinaryUtility.Deserialize<float[]>(System.IO.Path.Combine(Application.dataPath, "Project/Resources/vertices.bytes"));
        this.pVerticesSource = (float*)(this.nativeVerticesSource = new NativeArray<float>(vertices, Allocator.Persistent)).GetUnsafePtr();
        var normals = BinaryUtility.Deserialize<float[]>(System.IO.Path.Combine(Application.dataPath, "Project/Resources/normals.bytes"));
        this.pNormalsSource = (float*)(this.nativeNormalsSource = new NativeArray<float>(normals, Allocator.Persistent)).GetUnsafePtr();
        var tangents = BinaryUtility.Deserialize<float[]>(System.IO.Path.Combine(Application.dataPath, "Project/Resources/tangents.bytes"));
        this.pTangentsSource = (float*)(this.nativeTangentsSource = new NativeArray<float>(tangents, Allocator.Persistent)).GetUnsafePtr();
        #endregion
    }

    protected override void OnDestroy()
    {
        this.nativeVerticesSource.Dispose();
        this.nativeNormalsSource.Dispose();
        this.nativeTangentsSource.Dispose();

        base.OnDestroy();
    }

    protected unsafe override void OnUpdate()
    {
        #region NativeArray 確保
        var entities = this.query.ToEntityArray(Allocator.TempJob);
        var meshDataArray = this.query.ToComponentDataArray<ChunkMeshData>(Allocator.TempJob);
        #endregion

        #region メッシュの頂点数をカウント
        var countVerticesJob = new CountVerticesJob
        {
            verticesSourceCount = this.nativeVerticesSource.Length,
            meshDataArray = meshDataArray
        };
        var jobHandle = countVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        jobHandle.Complete();
        #endregion

        #region カウント数→頂点バッファを確保→バッファポインタを ComponentData に代入
        this.entityMeshDataList.Clear();
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var meshData = meshDataArray[entityIndex];
            var countVector3 = meshData.vertexCount / 3;
            var entityMeshData = new EntityMeshData {
                nativeVertices = new NativeArray<Vector3>(countVector3, Allocator.TempJob),
                nativeNormals = new NativeArray<Vector3>(countVector3, Allocator.TempJob),
                nativeTangents = new NativeArray<Vector4>(countVector3, Allocator.TempJob),
            };
            this.entityMeshDataList.Add(entityMeshData);
            meshData.pVerticesVector3 = (float*)entityMeshData.nativeVertices.GetUnsafePtr();
            meshData.pNormalsVector3 = (float*)entityMeshData.nativeNormals.GetUnsafePtr();
            meshData.pTangentsVector4 = (float*)entityMeshData.nativeTangents.GetUnsafePtr();
            meshDataArray[entityIndex] = meshData;
        }
        #endregion

        #region 頂点バッファに頂点データを塗る
        var fillVerticesJob = new FillVerticesJob
        {
            pVerticesSource = this.pVerticesSource,
            pNormalsSource = this.pNormalsSource,
            pTangentsSource = this.pTangentsSource,
            sourceCount = this.nativeVerticesSource.Length,
            meshDataArray = meshDataArray
        };
        var fillJobHandle = fillVerticesJob.Schedule(arrayLength: meshDataArray.Length, innerloopBatchCount: 1);
        fillJobHandle.Complete();
        #endregion

        #region 頂点バッファ→マネージド配列→メッシュ作成→メッシュ法線・接線の計算
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            var entityMeshData = this.entityMeshDataList[entityIndex];
            var vertices = entityMeshData.nativeVertices.ToArray();
            var normals = entityMeshData.nativeNormals.ToArray();
            var tangents = entityMeshData.nativeTangents.ToArray();

            var mesh = new Mesh();
            mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
            mesh.vertices = vertices;
            mesh.normals = normals;
            mesh.tangents = tangents;
            int[] triangles = new int[vertices.Length];
            for (int vertexIndex = 0; vertexIndex < vertices.Length; vertexIndex++)
            {
                triangles[vertexIndex] = vertexIndex;
            }
            mesh.SetIndices(triangles, MeshTopology.Triangles, submesh: 0, calculateBounds: true);
            var meshFilter = EntityManager.GetComponentObject<MeshFilter>(entity);
            meshFilter.mesh = mesh;

            entityMeshData.nativeVertices.Dispose();
            entityMeshData.nativeNormals.Dispose();
            entityMeshData.nativeTangents.Dispose();
        }
        this.entityMeshDataList.Clear();
        #endregion

        #region entity から marker の除去
        for (int entityIndex = 0; entityIndex < entities.Length; entityIndex++)
        {
            var entity = entities[entityIndex];
            EntityManager.RemoveComponent(entity, ComponentType.ReadOnly<CreateMeshMarker>());
        }
        #endregion

        #region NativeArray 開放
        meshDataArray.Dispose();
        entities.Dispose();
        #endregion
    }

    EntityQuery query;
    
    NativeArray<float> nativeVerticesSource;
    NativeArray<float> nativeNormalsSource;
    NativeArray<float> nativeTangentsSource;
    float* pVerticesSource;
    float* pNormalsSource;
    float* pTangentsSource;

    List<EntityMeshData> entityMeshDataList = new List<EntityMeshData>();

    class EntityMeshData
    {
        public NativeArray<Vector3> nativeVertices;
        public NativeArray<Vector3> nativeNormals;
        public NativeArray<Vector4> nativeTangents;
    }
}

いったん、この状態で負荷を分散するように作ってみましょう。

考えてみて難しいと思う部分は、処理をしながらフレームレートを上回るときに手を止めるという操作ができないということ
Instantiate に 200 ms と無視できない時間を要し
CreateMeshSystem に 1218 ms と最も大きな時間を要しています。

エンティティの数は 8x8x8 個ですので、概算で entity 一個あたり 2.76 ms 要しています。

一つ考えたのは、作成に使う値をキューに詰めて、必ず 1frame 1 entity 以上は作成するようポップして
最初は 1 entity の時間を計測し
次からは処理時間 + 余った時間を見て、それを全部埋める entity 数を算出し、次の frame ではその entity 数を処理
を繰り返す、つまりは次に処理するべき entity 数を endOfFrame で決定するというもの

簡単に作ってみます。

まずはキューに詰めてフレームに分散する処理

    IEnumerator OnStartOfFrame()
    {
        while (true)
        {
            yield return null;
            this.startFrame = Time.realtimeSinceStartup;

            #region 作成対象の MeshObject 位置を Enqueue
            if (Input.GetKeyDown(KeyCode.Space))
            {
                for (var x = 0; x < ChunkManager.ChunkSizeX; x++)
                {
                    for (var z = 0; z < ChunkManager.ChunkSizeZ; z++)
                    {
                        for (var y = 0; y < ChunkManager.ChunkSizeY; y++)
                        {
                            var position = new Vector3(ChunkManager.ChunkSizeX * x * 2, ChunkManager.ChunkSizeY * y, ChunkManager.ChunkSizeZ * z * 1.732f);
                            this.createMeshObjectQueue.Enqueue(position);
                        }
                    }
                }
            }
            #endregion

            #region Queue から位置を取り出してMeshObjectをInstantiate
            this.createMeshObjectCount = 0;
            for (int dequeueIndex = 0; dequeueIndex < this.dequeueCount; dequeueIndex++)
            {
                if (0 == this.createMeshObjectQueue.Count) {
                    break;
                }
                CreateMeshObject(this.createMeshObjectQueue.Dequeue());
                this.createMeshObjectCount = dequeueIndex + 1;
            }
            #endregion
        }
    }

これで 1frame 1個
1秒 60 entity が作成されていく様子が確認できました。

続いて処理時間を entity 数で割って、 1 entity あたりの処理時間を出し
その1処理時間で、全処理時間 + 余っている時間 を割って、次のフレームでの dequeueCount を決定します。

    IEnumerator OnEndOfFrame()
    {
        while (true)
        {
            yield return new WaitForEndOfFrame();
            this.endFrame = Time.realtimeSinceStartup;

            #region 処理時間を見て次のフレームの dequeueCount を決定
            if (0 < this.createMeshObjectCount)
            {
                var updateTime = (this.endFrame - this.startFrame);
                var entityTime = updateTime / this.createMeshObjectCount;
                var targetFrameSecond = 1f / Application.targetFrameRate;
                this.dequeueCount = Mathf.FloorToInt(Mathf.Lerp(this.dequeueCount, targetFrameSecond / entityTime, 0.7f));
            }
            if (0 == this.dequeueCount)
            {
                this.dequeueCount = 1;
            }
            #endregion
        }
    }

GCがあるないでギザギザしていますが…巨大なスパイクだけは回避できています。

f:id:simplestar_tech:20190612081957p:plain
周辺フレームに分散して CreateMesh が 60fps をターゲットに行えている様子

動作確認する

こんな処理になりました。

動くサンプルはこちら
github.com

同じこと考えていた人はご参考までに~

Unity:Amazon DynamoDB を C# から利用する

高速読み書きがスケーラブルなデータベース Dynamo DB を Unity から利用する

現在のルームメンバー一覧を取得したいとか、これからジョインするルームを決めるときに、ルームに何人入っているのかなどを確認したいときがあるのです。
S3 にファイルとして情報を置く?…いえいえ、ここはデータベースの出番だと思います。

今回は DynamoDB というキーと値のセットでデータを格納するタイプのデータベースを Unity から利用してデータを取得する例を示します。

前知識

前回の記事では、Amazon S3 というオンラインストレージを Unity から利用して
ファイルパスを渡してS3の指定バケットにアップロードするとオブジェクトキーを取得する機能
S3バケットのオブジェクトキーを指定するとダウンロードしたbyte配列を受け取る機能
いずれも非同期で実行する方法を示し、動作確認も行いました。
simplestar-tech.hatenablog.com

ここからは Unity から AWS を利用するための Cognito による認証・認可のハードルを越えている読者に向けて説明を続けます。
理解できずに振り落とされてしまった場合は、前回の記事を参照していただければと思います。

AWS SDK DynamoDB を使うための準備

Assets 以下に次の URL から net45 フォルダの dll を配置して、DynamoDB 関係の実装を利用できるようにします。
AWSSDK.DynamoDBv2

そして S3 のクライアントのときと同じく、Cognito の SignIn イベントにハンドラを登録して DynamoDBContext を作成します。
あとは以下のロジックを組みます。

AmazonDynamoDBClient.cs

using Amazon;
using Amazon.CognitoIdentity;
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;
using Amazon.DynamoDBv2.DocumentModel;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Events;

namespace AWS
{
    public class AmazonDynamoDBClient : MonoBehaviour
    {
        #region Scene Components
        [SerializeField] AmazonCognitoSignInGUI cognitoSignInGUI;
        #endregion        

        void Start()
        {
            this.cognitoSignInGUI.onSignInSuccess += OnSignIn;            
        }

        void OnSignIn(CognitoAWSCredentials credentials)
        {
            this.dynamoDBClient = new Amazon.DynamoDBv2.AmazonDynamoDBClient(credentials, this.resourceRegion);
            this.dynamoDBContext = new DynamoDBContext(this.dynamoDBClient);
        }

        public async Task QueryAsync<T>(string partitionKey, QueryOperator queryOperator, IEnumerable<string> sortKeys, UnityAction<List<T>> onQuerySuccess)
        {
            var appQuery = dynamoDBContext.QueryAsync<T>(partitionKey, queryOperator, sortKeys);
            onQuerySuccess?.Invoke(await appQuery.GetRemainingAsync());
        }

        public async Task LoadAttributes<T>(string partitionKey, string sortKey, UnityAction<T> onLoadSuccess)
        {
            onLoadSuccess?.Invoke(await this.dynamoDBContext.LoadAsync<T>(partitionKey, sortKey));
        }

        public async Task SaveAttributes<T>(T attributes, UnityAction onSaveSuccess)
        {
            await this.dynamoDBContext.SaveAsync<T>(attributes);
            onSaveSuccess?.Invoke();
        }

        int bookID = 1001;

        RegionEndpoint resourceRegion = RegionEndpoint.APNortheast1;
        IAmazonDynamoDB dynamoDBClient = null;
        IDynamoDBContext dynamoDBContext = null;
    }
}

AmazonDynamoDBAttributes.cs

using Amazon.DynamoDBv2.DataModel;
using System.Collections.Generic;

namespace AWS.DynamoDBAttributes
{
    [DynamoDBTable("CubeWalkC1")]
    public class Rooms
    {
        [DynamoDBHashKey] public string PartitionKey { get; set; } = "Rooms";
        [DynamoDBRangeKey] public string SortKey { get; set; } = "XXX#XXX#XXXXXXX";
        [DynamoDBProperty("Players")] public List<string> DatePlayerIDs { get; set; } = new List<string>();
    }

    [DynamoDBTable("CubeWalkC1")]
    public class Players
    {
        [DynamoDBHashKey] public string PartitionKey { get; set; } = "Players";
        [DynamoDBRangeKey] public string SortKey { get; set; } = "20XX-XX-XXTXX:XX:XX#xxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
        [DynamoDBProperty] public string PlayerName { get; set; }
    }
}

Scene Component との接続は次の通り

f:id:simplestar_tech:20190602164129p:plain
Scene Component 接続

認証済みユーザーに割り当てる IAM ロールにて DynamoDB の GetItem x 特定テーブル ARN の指定を加えます。
以下のロール編集画面の通り

これけっこう重要なナレッジかな?DynamoDBContext には DescribeTable は必須みたい(ドキュメントのどこかに書かれているかな?)

f:id:simplestar_tech:20190602165421j:plain
DynamoDB のインラインポリシーの設定例

これで認証されたユーザーから CubeWalkC1 テーブルからアイテム情報を受け取ったり、配置できたりするようになります。

テスト

テストに使ったコードがこちら
TestAWSGUI.cs

using UnityEngine;
using UnityEngine.UI;
using AWS;
using AWS.DynamoDBAttributes;
using System.IO;
using Amazon.DynamoDBv2.DocumentModel;

public class TestAWSGUI : MonoBehaviour
{
    #region UI Connection
    [SerializeField] Button buttonPut;
    [SerializeField] Button buttonGet;
    #endregion

    #region Scene Components
    [SerializeField] AmazonS3Client s3Client;
    [SerializeField] AmazonDynamoDBClient dynamoDBClient;
    #endregion

    void Start()
    {
        this.buttonPut.onClick.AddListener(this.OnPut);
        this.buttonGet.onClick.AddListener(this.OnGet);
    }

    async void OnGet()
    {
        await this.s3Client.GetObjectDataAsync("ap-northeast-1:xxxxxxxxx-xxxxxx-xxxxxxx-xxxxx-xxxxxxxxxxxxxxx/Miraikomachi.vrm", (bytes) =>
        {
            File.WriteAllBytes(@"C:\Users\simpl\Downloads\VRM\MiraikomachiVRM-master\Miraikomachi2.vrm", bytes);
        });

        await this.dynamoDBClient.QueryAsync<Rooms>("Rooms", QueryOperator.BeginsWith, new string[] { "XXX" }, (list) => {
            foreach (var item in list)
            {
                foreach (var playerID in item.DatePlayerIDs)
                {
                    Debug.Log(playerID);
                }
                Debug.Log(item.SortKey);
            }
        });
    }

    async void OnPut()
    {
        await this.s3Client.PutObjectFileAsync(@"C:\Users\simpl\Downloads\VRM\MiraikomachiVRM-master\Miraikomachi.vrm", (objectKey) =>
        {
            Debug.Log(objectKey);
        });

        await this.dynamoDBClient.SaveAttributes(new Rooms { DatePlayerIDs = new System.Collections.Generic.List<string> { "player1", "player2" } }, () =>
        {
            Debug.Log("Save Rooms");
        });

        await this.dynamoDBClient.SaveAttributes(new Players { PlayerName = "player1" }, () =>
        {
            Debug.Log("Save Players");
        });
    }
}

実行すると以下のログが流れました。
期待通り、DynamoDB への書き込み、読み取りが機能している様子(クライアントからルーム情報いじれるのはセキュリティ的にどうかと考えてみたけど、ウソのジョインをいっぱい送ることも可能なのでどこで更新しても壊せるかな)

f:id:simplestar_tech:20190602170652j:plain
上記テストコードの実行結果

まとめ

DynamoDB を Unity から操作できました!