What is the best practice of creating mesh via MeshData?

Hi! Can anybody please tell me, what is a good practice to create a mesh using MeshData (and Mesh.AllocateWritableMeshData) in situation when I dont know the vertex and indicies count at IJob.Execute start?

The examples in documentation tell that I can use SetVertexBufferParams and then get its NativeArrays. But it requires the vertex count, which is unknown for me at creation time.

Unity example:

 data.SetVertexBufferParams(12,
           new VertexAttributeDescriptor(VertexAttribute.Position),
           new VertexAttributeDescriptor(VertexAttribute.Normal, stream:1));

So what is a good practice to create a mesh inside JobSystem and with usage of Burst?

I apologize that

  1. I can create a NativeArray with extra size
  2. Fill this NativeArray with data and calculate the needed VertexBuffer length
  3. Create the VertexBuffer with correct size
  4. Slice NativeArray and CopyTo it to VertexBuffer’s NativeArray.

Is it sounds like the correct way? Cause I have 2 allocations for each channel (2 Indicies, 2 Normals, 2 UVs and etc, where 1 is for WriteBuffer and 1 for my Temp vertices data.).

2 Likes

That also depends on the use case, in my case I calculated the numbers before i created the mesh itself. It’s a 2d voxel grid, so the calculation was simple.

Yes, I understand, but in my case it is the slice of the mesh into two meshes. so I cut every triangle and decide which of two parts of mesh it belongs to, and then I cap holes and triangulate them. so I dont know the final count of vertices until the end

I don’t use MeshData at all. I build the vertices and indices as NativeLists (resizable) in jobs, then apply them to the mesh in one go. As long as you’re using NativeList/NativeArray, it won’t allocate anything in Unity (driver might, and of course they need to be stored somewhere on the GPU). NativeList is resizable, although you may wish to preallocate some sensible defaults: Struct NativeList<T> | Collections | 0.11.0-preview.17

For example:

struct MyVertex {
    float3 position;
    float3 normal;
    float2 uv; }
static readonly VertexAttributeDescriptor[] vertexLayout =  {
    new VertexAttributeDescriptor { attribute = VertexAttribute.Position, format = VertexAttributeFormat.Float32, dimension = 3 },
    new VertexAttributeDescriptor { attribute = VertexAttribute.Normal, format = VertexAttributeFormat.Float32, dimension = 3 },
    new VertexAttributeDescriptor { attribute = VertexAttribute.TexCoord0, format = VertexAttributeFormat.Float32, dimension = 2 } };
struct MyJob : IJob {
    NativeList<MyVertex> vertices;
    NativeList<int> indices;
    void Execute() { /* fill up the vertices and indices lists using .Add() */ } }

// somewhere in your initialization code
Mesh mesh = new Mesh();
mesh.hideFlags = HideFlags.DontSaveInBuild | HideFlags.DontSaveInEditor;
mesh.MarkDynamic(); // or not, depends on your use case

// somewhere on the main thread at the beginning of the frame...
MyJob job = new MyJob();
job.vertices = new NativeList<MyVertex>(512 /* preallocated size, but can grow beyond this */, Allocator.TempJob);
job.indices = new NativeList<int>(512 /* preallocated size, but can grow beyond this */, Allocator.TempJob);
JobHandle handle = job.Schedule();
JobHandle.ScheduleBatchedJobs(); // depends on your threading pattern whether you want this or not

// somewhere else on the main thread at the end of the frame or maybe the next frame...
handle.Complete();
mesh.Clear(true);
mesh.SetVertexBufferParams(job.vertices.Length, vertexLayout);
mesh.SetVertexBufferData<MyVertex>(job.vertices, 0, 0, job.vertices.Length, 0, 0);
mesh.SetIndexBufferParams(job.indices.Length, IndexFormat.UInt32 /* or 16, and use ushort instead of int */);
mesh.SetIndexBufferData<int>(job.indices, 0, 0, job.indices.Length, 0);
mesh.subMeshCount = 1;
SubMeshDescriptor smd = new SubMeshDescriptor {
    topology = MeshTopology.Triangles,
    vertexCount = job.vertices.Length,
    indexCount = job.indices.Length };
mesh.SetSubMesh(0, smd, 0);
mesh.UploadMeshData(/* true or false depending on your use case */);

// don't forget these like I did when originally writing this post!
job.vertices.Dispose();
job.indices.Dispose();

Here’s my code (messy, and the comments are highly trained in stealth ninjitsu, so you might not see them ):
https://gitlab.com/burningmime/easybuilding/-/blob/master/Packages/building/src/jobs/MeshBuilder.cs

There are various ways to optimize this (eg different MeshUpdateFlags, and calculating the bounds yourself in the job). Your call as to how far you want to go with those; investigate the mesh API.

11 Likes

Thank you very much for this example, I will try this way. I did not want to use Native Lists cause they are under preview in Entities package. But maybe it is ok to use them.

MeshData is a little silly, I can’t see any advantage in using it when you don’t know the vertex count in advance. No wonder the example code in the docs is a tetrahedron.

1 Like

You can use NativeLists if you don’t know the initial vertex count. The main advantage of this new API is availability to create mesh with JobSystem

2 Likes