Howdy.
I am having issues wrapping my head around how to structure my code utilizing entities, jobs and burst. I feel like it’s possible to do the following:
-
Given a user authored value for how big a heightfield should be
-
Off of the main thread
-
Utilize burst so that each vertex in the field gets it’s height value calculated in parallel w/ Unity’s math.noise function
-
Utilize burst to generate the triangle indices for a regular triangle strip in parallel
-
After the previous two jobs have finished, copy the data out of a dynamics buffer (or native array) and into a RenderMesh
-
Only execute the above once, I will eventually want to dynamically add in a Tiling concept where I will want to add/remove this type of mesh on the fly as the character moves around
I’m really just after the most efficient way to generate a regular triangle network heightfield, so it’s possible my line of thinking is off above.
Here is my naive approach that forces me to use WithoutBurst & Run accordingly and outputs the mesh as expected. I’ve failed to break this code into what I am guessing is the correct flow I outlined previously.
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using System;
[AddComponentMenu("OH/NoiseSeed")]
public class NoiseSeedAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
public int Seed = 1337; // todo actually use this value
public int Size = 10;
public Material Material;
public void Convert(Entity entity, EntityManager entityManager, GameObjectConversionSystem conversionSystem)
{
entityManager.AddComponentData(entity, new NoiseSeed
{
Seed = Seed,
Size = Size,
Loaded = false
});
entityManager.AddSharedComponentData(entity, new SimpleMeshRenderer
{
Mesh = new Mesh(),
Material = Material
});
}
}
public struct NoiseSeed : IComponentData
{
public int Seed;
public int Size;
public bool Loaded; // refactor out
}
public struct SimpleMeshRenderer : ISharedComponentData, IEquatable<SimpleMeshRenderer>
{
public Mesh Mesh;
public Material Material;
// todo
public bool Equals(SimpleMeshRenderer other)
{
return false;
}
// todo https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-5.0
public override int GetHashCode()
{
return 1;
}
}
public class NoiseBuilderSystem : SystemBase
{
private EntityQuery _query;
protected override void OnCreate()
{
base.OnCreate();
var queryDescription = new EntityQueryDesc
{
All = new ComponentType[]
{
ComponentType.ReadWrite<NoiseSeed>(),
ComponentType.ReadWrite<SimpleMeshRenderer>(),
}
};
_query = GetEntityQuery(queryDescription);
}
protected override void OnUpdate()
{
Entities
.WithStoreEntityQueryInField(ref _query)
.WithoutBurst() // todo refactor to WithBurst
.ForEach((ref NoiseSeed seed, in SimpleMeshRenderer meshRenderer) =>
{
if(!seed.Loaded)
{
LoadSeed(ref seed, in meshRenderer);
}
Graphics.DrawMesh(meshRenderer.Mesh, new Vector3(), Quaternion.identity, meshRenderer.Material, 0);
})
.Run(); // todo refactor to ScheduleParallel()?
}
private static void LoadSeed(ref NoiseSeed noiseSeed, in SimpleMeshRenderer meshRenderer)
{
// verts, I expect this can be burst'd
var rowSize = noiseSeed.Size;
var colSize = noiseSeed.Size;
var vertices = new Vector3[noiseSeed.Size * noiseSeed.Size];
for (int row = 0, index = 0; row < rowSize; row++)
{
for (int col = 0; col < noiseSeed.Size; col++, index++)
{
var heightPoint = new float2(row, col);
float height = noise.snoise(heightPoint);
vertices[index] = new Vector3(row, height, col);
}
}
// tris, I expect this can be burst'd
var quadRowSize = rowSize - 1;
var quadColSize = colSize - 1;
var triangles = new int[(quadRowSize * quadColSize) * (2*3)];
for (int quadRow = 0, triangleIndex = 0; quadRow < quadRowSize; quadRow++)
{
for (int quadCol = 0; quadCol < quadColSize; quadCol++, triangleIndex += 6)
{
var topLeft = (quadRow * rowSize) + quadCol;
var topRight = (quadRow * rowSize) + (quadCol + 1);
var bottomLeft = ((quadRow + 1) * rowSize) + quadCol;
var bottomRight = ((quadRow + 1) * rowSize) + (quadCol + 1);
triangles[triangleIndex] = topLeft; // 1st triangle
triangles[triangleIndex + 1] = topRight;
triangles[triangleIndex + 2] = bottomLeft;
triangles[triangleIndex + 3] = topRight; // 2nd triangle
triangles[triangleIndex + 4] = bottomRight;
triangles[triangleIndex + 5] = bottomLeft;
}
}
// mesh, I presume this is where I would wait for the previous two handles to finish and perform a copy. I'm also confused on how to translate from my entity's DynamicBuffer<float3> to the Mesh's verticies Vector3[]
meshRenderer.Mesh.vertices = vertices;
meshRenderer.Mesh.triangles = triangles;
meshRenderer.Mesh.RecalculateBounds(); // I believe this can just be set directly with a constant extent {.5, 1 .5}
meshRenderer.Mesh.RecalculateNormals(); // I eventually plan on calculating the Normals in a similar manner / at the same time as the vertex's
meshRenderer.Mesh.RecalculateTangents();
// cache
noiseSeed.Loaded = true;
}
}
}
And here is where I’m currently left off scratching my head in the refactor.
public struct VertexBufferElement : IBufferElementData
{
public float3 Value;
}
public struct TriangleIndexBufferElement : IBufferElementData
{
public int Value;
}
// .. system update function:
var commandBuffer = _entityCommandBufferSystem
.CreateCommandBuffer()
.AsParallelWriter();
var spawnTileJobHandle = Entities
.WithName("SpawnTile")
.WithBurst(FloatMode.Default, FloatPrecision.Standard, true)
.ForEach((Entity entity, int entityInQueryIndex, in NoiseSeed noiseSeed) =>
{
var vertexBuffer = commandBuffer.AddBuffer<VertexBufferElement>(entityInQueryIndex, entity);
vertexBuffer.Length = noiseSeed.Size * noiseSeed.Size;
var rowSize = noiseSeed.Size;
var colSize = noiseSeed.Size;
for (int row = 0, index = 0; row < rowSize; row++)
{
for (int col = 0; col < noiseSeed.Size; col++, index++)
{
var heightPoint = new float2(row, col);
float height = noise.snoise(heightPoint);
vertexBuffer[index] = new VertexBufferElement
{
Value = new float3 (row, height, col)
};
}
}
}).Schedule(Dependency);
spawnTileJobHandle.Complete();
Entities
.ForEach((DynamicBuffer<VertexBufferElement> vertexBuffer, ref SimpleMeshRenderer meshRenderer) =>
{
meshRenderer.Mesh = new Mesh
{
vertices = new Vector3[vertexBuffer.Length]
};
for(int i = 0; i < vertexBuffer.Length; i++)
{
meshRenderer.Mesh.vertices[i] = new Vector3(vertexBuffer[i].Value.x, vertexBuffer[i].Value.y, vertexBuffer[i].Value.z);
}
});