/// <summary>/// ダウンロード完了イベントとキュー詰めは分けるべき @todo/// </summary>internalvoid 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;
}
#region 作成対象のエンティティを Enqueueif (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
遅い…なんとか高速化できないか
高速化について、次のように、中央から順に周辺チャンクを結合していくループを回すのはどうかと考えた
#region 作成対象のチャンク情報を Enqueueif (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
usingstatic Unity.Mathematics.math;
/// <summary>Returns the result of rotating a vector by a unit quaternion.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
publicstatic 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;
}
# ポインターのポインターで世界を 8 byte アドレスに
今回は 8 x 8 x 8 要素のチャンクを作り、これを使い回す形で 8 x8x8 個のエンティティを作成しています。
チャンクにインデックスを振るなら byte にする
現在はエンティティを個別に識別するための要素は Vector3 の位置だけ
その x, y, z 要素はそれぞれ Chunk の SideX, Y, Z の整数倍となっているので、単純に割ってから 256 で余りを得るとインデックスになる
ただ、ランタイムで割り算は行いたくないので、ここはエンティティ作成時に byte index を設定することにする
コードを書く前に検算ですが、3次元を一次元配列にしている現在
x 方向に一つ足すと (byte.MaxValue + 1) * (byte.MaxValue + 1) 足すことを意味する
困ったことに x 方向に一つ足すべきなのかを判別する計算がわからない
今は chunkData 内の index でしかなく、もし境界の要素だったとして、そこから一つ足した場合でも chunkData 内に納まるインデックスだったら?
ね?
判別不可能でしょう。
そこで、chunkData 内の index を受け取るのをやめて、もっと情報量のボリュームがある三次元 index x, y, z を受け取るようにするのはどうか?
こうすると例えば y を一つたして chunk の y 最大を n 回オーバーするときに ppChunk 内でいくつ移動すればよいかがわかる
これにしましょう。 リファクタリングします。
このときはまだこのように、チャンクをまたぐ部分で困った状態となっています。
staticbyte* 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
{
// 困ったreturnnull;
}
}
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;
internalunsafeclass CreateMeshSystem : ComponentSystem
{
/// <summary>/// チャンク情報から頂点数をカウント/// </summary>
[BurstCompile]
unsafestruct CountVerticesJob : IJobParallelFor
{
internal NativeArray<ChunkMeshData> meshDataArray;
internalint verticesSourceCount;
publicvoid 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]
unsafestruct FillVerticesJob : IJobParallelFor
{
[NativeDisableUnsafePtrRestriction] [ReadOnly] internalfloat* pVerticesSourceVector3;
internal NativeArray<ChunkMeshData> meshDataArray;
publicvoid 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));
}
}
}
}
protectedoverrideunsafevoid OnCreate()
{
base.OnCreate();
this.query = GetEntityQuery(new EntityQueryDesc
{
All = new[] { ComponentType.ReadOnly<CreateMeshMarker>(),
ComponentType.ReadWrite<ChunkMeshData>() },
});
#region モデルの形状を定義→NativeArray確保int[] indices = newint[]
{
0, 1, 2,
3, 4, 5,
0, 2, 4,
4, 3, 0,
2, 1, 5,
5, 4, 2,
1, 0, 3,
3, 5, 1
};
constfloat 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
}
protectedoverridevoid OnDestroy()
{
this.nativeVerticesSource.Dispose(); // NativeArray 開放base.OnDestroy();
}
protectedunsafeoverridevoid 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 = newint[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 で同じ結果を得られることを確認しました。
カクつかない仕組みを作る
まずはカクつかせるようにメッシュを複雑にして entity の数を増やしてみましょう。
上記のコードを次の通り変更します。
publicclass ChunkManager
{
publicconstint ChunkSizeX = 16;
publicconstint ChunkSizeY = 16;
publicconstint ChunkSizeZ = 16;
publicconstfloat CubeSide = 2f;
}
#region チャンク情報の定義
var chunkData = newint[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
publicvoid 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;
}
}
}
}
}
}
さらに entity を増やします。
コードはこんな変更をします。
privateunsafevoid 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);
}
privatevoid 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);
}
}
}
}
}
プロファイラを確認すると大変なスパイクが発生していることが確認できました。
normal と tangent の計算に時間を要しているようです。
normal と tangent をあらかじめ与えてコピーするようにしてみたらどうなるでしょうか?