# まえがき
Unity でキューブを敷き詰めた世界で、VRMオンラインチャットできることを目指して仕事の合間に技術調査してます。(かれこれ2年くらい)
Unity も 2017.1 から気づけば 2019.1 ですね。
キューブ一つひとつをメッシュオブジェクトにすると、下記を参考にわずか 20 x 20 x 20 個のキューブをシーンに配置するだけで、描画レートは 20 fps を下回ります。
Unity に限った話ではありませんが、参考までにこちら2年前の記事ですが…
六角柱を敷き詰めたマイクロワールドの構築(メッシュの結合とマルチマテリアル処理) - simplestarの技術ブログ
当時はメッシュを結合すれば爆速になることを確認して、とにかくメッシュを結合することを決めました。
結合してしまえば 215 x 215 x 215 個のキューブをシーンに配置しても 1087 fps 出た。
キューブまたは構成要素一つひとつのロジック計算量の見積もりが甘く…人間が面白いと感じる速度で世界は変化していかないわけです…
そこで目をつけたのが PC のメニーコア化と Unity ECS です。
モンスター計算マシンと ECS なら、面白いと感じる速度で世界が変化するかも…と、そこで結果を出すために技術調査を続ける毎日です。
話を元に戻しますが、私達の生きているこの世界に果てはないそうで、地平線の向こう側まで直進し続けると元の場所に戻ってくるそうじゃないですか。
宇宙すら、不可能とされる光速を越えて直進し続けると出発点に戻ってくるとか
そこで 1 byte の 255 + 1 = 0 のオーバーフローを利用して、世界を 256 x 256 x 256 結合メッシュとする世界を考えます。
そろそろ本題ですが、結合メッシュの世界の最小構成要素(キューブ)配列を 256 x 256 x 256 (16,777,216) 要素の配列にして表現したく…
プログラマーの手段でこれを扱うと int** とポインターのポインターになるわけです。(キューブの情報を int とするとね)
ECS はその原理からマネージド配列の操作を許しませんので(NativeArray または ポインター)、どうしてもこのポインターのポインターを渡して仕事をさせたい訳なのです。
実はもうできそうなことわかっているので、その動作確認というものをこの記事で行ってみたいと思います。
できなかったらこの記事は完成しません。(恥ずかしい…)
# ECS によるメッシュ生成でキューブの配置
六角を廃して、最近作り直したキューブの世界…
動作確認できてよかったので、コードを移植していきます。
キューブ一つに 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,
回転角度の適用は下の図の通りです。
関連ツィートはこちらから
ある狙いのために頂点情報を手打ち…今日はここまで…疲れます pic.twitter.com/6sxAtb6BFp
— Simplestar (@lpcwstr) January 13, 2019
このとき手打ちした頂点位置配列がこちら
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),
キューブを敷き詰めるときに重要なのが、周囲のキューブの情報の取得です。
処理を結合メッシュチャンクごとに分けた以上、チャンクをまたぐインデックスアクセスだと範囲外エラーとなります。
実はこの問題を解決するために、ポインターのポインターが必要になります。
このまま解説のみで形にすることは難しいので、一度無回転のキューブだけを敷き詰め
周囲のチャンクの情報を取得しようとして困った状況というものを作ってみます。
頂点情報は上で作ったもので交換して、三角柱をキューブにして確認していきましょう。
頂点情報はこちらに交換しました。
#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
面と頂点数はとても勿体ない状況であることを確認します。
メッシュ用の頂点数をカウントする、頂点情報を埋める処理において、周囲のキューブ情報を参照して、描画されることのない面を削る処理を追加してみます。
作る前に頭で想像すると
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面全部に適用してみるとひとまずはカリングされます。この結果を正しいと確信できる人は、私と同じこと考えているに違いない
# ポインターのポインターで世界を 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; } }
# 周囲の結合メッシュの情報を参照
周囲の結合メッシュ…つまりは 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 内でいくつ移動すればよいかがわかる
これにしましょう。
リファクタリングします。
このときはまだこのように、チャンクをまたぐ部分で困った状態となっています。
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 方向のチャンク参照がすべて失敗している様子
動作確認していきます。
# 動作確認
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 }
周辺チャンクへのアクセスが正しく行えるようになり、メッシュも期待通り削れました。
動作している絵
# まとめ
キューブの結合メッシュの元となるチャンクデータは 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