simplestarの技術ブログ

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

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