Search Unity

Using ECS to update a mesh.

Discussion in 'Entity Component System' started by Mr-Mechanical, Aug 5, 2018.

  1. Mr-Mechanical

    Mr-Mechanical

    Joined:
    May 31, 2015
    Posts:
    507
    What is the most performant approach to this? It seems to be a major hit for performance to update mesh.vertices.
     
  2. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,518
    Kind of a vague question. Can you be more specific, as in how your question applies to ecs specifically?
     
    EdRowlett-Barbu likes this.
  3. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,769
  4. SugoiDev

    SugoiDev

    Joined:
    Mar 27, 2013
    Posts:
    395
    Hopefully this will get much better when we can assign the Native* structures directly to the mesh, instead of having to copy over.

    Until then, what @Antypodish posted is the best we have.
    If you need more performance, you could maybe do some unsafe copying to avoid all the checks that copying the Native* structures does, but you won't get away with the cost of setting the vertices themselves.


    If your mesh is (kinda) sparse, you could maybe get a bit more performance from using this super specialized list: https://github.com/Nordeus/Unite2015/blob/master/VaryingList/VaryingList.cs

    I use it myself and got a nice improvement with a mesh that is about 80% empty.
     
  5. Mr-Mechanical

    Mr-Mechanical

    Joined:
    May 31, 2015
    Posts:
    507
    Really hope Unity team gets on this. ECS could be really awesome for ultra-fast mesh manipulation.
     
  6. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    You can already do high performance mesh manipulation in jobs, but I'm not sure how you can really get around the bottleneck of pushing your mesh data back to the GPU.

    Here's a demo of what I've been working on at the moment with meshes. Generating (greedy) meshes on the fly while doing back face culling every frame.

     
    bb8_1, Deleted User and Antypodish like this.
  7. Antypodish

    Antypodish

    Joined:
    Apr 29, 2014
    Posts:
    10,769
    tertle I have seen your screenshots, but haven seen video. Nice one.
     
  8. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    I should add the mesh demo Antypodish actually posted is quite bad (no fault of his own, it's Unity's own demo afterall).

    Change Mesh Vertices & Normals Every Frame uses NativeArray.CopyTo(T[]) which is really quite slow as it iterates the nativearray and copys the elements 1 at a time.

    If you are manipulating more than 1 mesh it will add multiple ms per frame, significantly reducing your fps.
     
    bb8_1 likes this.
  9. Mr-Mechanical

    Mr-Mechanical

    Joined:
    May 31, 2015
    Posts:
    507
    I found this performance hit to be substantial, is there an alternative to the slow copy? If not it should be added ASAP, as this is one of the main uses of ECS/Job System.
     
  10. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Yes, I've talked about it a bit before here: https://forum.unity.com/threads/allow-setting-mesh-arrays-with-nativearrays.536736/#post-3536400

    keijiro had a great demo of it here which is what I based this all off: https://github.com/keijiro/Firefly

    Either you just modify a T[] directly in a job using pointers (faster method) or do a memcopy (slightly slower, but cleaner, safer and shouldn't be your bottleneck anyway)

    I even benchmarked it further down, but i'll repost it here.

    Default NativeArray.CopyTo (2.40ms)



    Array.CopyTo - very similar to Marshal performance (0.15ms)



    My custom UnsafeNativeArray - directly editing the array, no copying (0.07ms)



    So relative performance between Array.Copy and directly editing array isn't huge, however it does halve your memory requirement as you don't require 2 copies of the array - not a big deal if you only need a single array but if you need a lot it's signficant.
     
    Last edited: Aug 6, 2018
    bb8_1, NotaNaN, Rewaken and 3 others like this.
  11. andywatts

    andywatts

    Joined:
    Sep 19, 2015
    Posts:
    112
    Nice video @tertle

    How are you doing the back face culling?

    I noticed you have the mesh in the scene view.
    Are you using the traditional MeshRenderer and or have you found a way to get MeshInstanceRenderer in the scene view?






     
  12. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Yes I'm using traditional mesh renderers as I found that they are more than twice as fast for unique meshes when you can't benefit from instancing.

    Using MeshRenderers (415 'fps', 2.4ms cpu, 0.7ms render)



    Using MeshInstanceRenderSystem (187 'fps', 5.4ms cpu, 1.9ms render)



    From my original post here: here: https://forum.unity.com/threads/meshinstancerendersystem-poor-performance.537834/

    As for my backface culling, I just do a very simple position check that disables faces. It doesn't do any tunnel checks or anything, I don't need it as this project is mostly limited to 2D from a top down perspective that just uses a voxel type landscape. You could totally do some type of flood fill like minecraft does though if you wanted.

    Code (CSharp):
    1.         [BurstCompile]
    2.         private struct CullFacesJob : IJobParallelFor
    3.         {
    4.             public float3 CullFrom;
    5.             public int3 ChunkSize;
    6.  
    7.             [ReadOnly] public EntityArray Entities;
    8.             [ReadOnly] public ComponentDataArray<GridPosition> Positions;
    9.             public ComponentDataArray<FaceCull> Culling;
    10.  
    11.             public NativeQueue<Entity>.Concurrent EntitiesToCull;
    12.          
    13.             public void Execute(int index)
    14.             {
    15.                 var facesToCull = new FaceCull();
    16.                 var position = Positions[index].Value;
    17.  
    18.                 if (CullFrom.x < position.x - ChunkSize.x) facesToCull.Right = true;
    19.                 if (CullFrom.x > position.x + ChunkSize.x + ChunkSize.x) facesToCull.Left = true;
    20.                 if (CullFrom.y < position.y - ChunkSize.y) facesToCull.Top = true;
    21.                 if (CullFrom.y > position.y + ChunkSize.y + ChunkSize.y) facesToCull.Bottom = true;
    22.                 if (CullFrom.z < position.z - ChunkSize.z) facesToCull.Back = true;
    23.                 if (CullFrom.z > position.z + ChunkSize.z + ChunkSize.z) facesToCull.Front = true;
    24.  
    25.                 if (!facesToCull.Equals(Culling[index]))
    26.                 {
    27.                     Culling[index] = facesToCull;
    28.                     EntitiesToCull.Enqueue(Entities[index]);
    29.                 }
    30.             }
    31.         }
    I generate each side of my mesh separately so I can easily put it back together every time I have to change which face is culled. The only real cost is calculating the indices offsets.

    Code (CSharp):
    1.         private void UpdateChunk(ChunkMesh chunk, FaceCull culling)
    2.         {        
    3.             for (var index = 0; index < 6; index++)
    4.             {
    5.                 if (culling.IsCulled(index))
    6.                     continue;
    7.              
    8.                 var face = chunk.MeshFaces.GetFace(index);
    9.                 face.Resize();
    10.  
    11.                 // Need to offset our indices
    12.                 var current = _indices.Count;
    13.                 var indices = face.Indices.List;
    14.                 _indices.AddRange(indices);
    15.  
    16.                 var indicesCount = _indices.Count;
    17.                 var offset = _vertices.Count;
    18.  
    19.                 // we could UnsafeNativeList increment this in a job for better performance?
    20.                 for (var i = current; i < indicesCount; i++)
    21.                     _indices[i] += offset;
    22.              
    23.                 _vertices.AddRange(face.Vertices.List);
    24.                 _normals.AddRange(face.Normals.List);
    25.                 _uvs.AddRange(face.UVs.List);
    26.             }
    27.  
    28.             var mesh = chunk.Presenter.Mesh;
    29.             mesh.Clear();
    30.  
    31.             if (_vertices.Count == 0)
    32.                 return;
    33.          
    34.             mesh.SetVertices(_vertices);
    35.             mesh.SetNormals(_normals);
    36.             mesh.SetUVs(0, _uvs);
    37.             mesh.SetTriangles(_indices, 0);
    38.          
    39.             _vertices.Clear();
    40.             _normals.Clear();
    41.             _uvs.Clear();
    42.             _indices.Clear();
    43.         }
     
    Last edited: Aug 8, 2018
    bb8_1 and andywatts like this.
  13. zrrz

    zrrz

    Joined:
    Nov 14, 2012
    Posts:
    40
    Hey @tertle would you mind giving some additional info on how you did the cubic voxel meshing in ECS? I've been trying to figure it out. Specifically, what parts are you doing in jobs, 1 pass or multiple, and how are you allocating memory for new chunks?
     
  14. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Hey @zrrz

    So my mesh components are allocated like this

    Code (CSharp):
    1. public struct Chunk : ISharedComponentData
    2. {
    3.     public NativeArray<short> Blocks;  
    4. }
    5.  
    6. public struct ChunkMesh : ISharedComponentData
    7. {
    8.     public NativeArray<BlockFaces> Faces;
    9.     public ChunkPresenter Presenter;
    10.     public MeshFaces MeshFaces;
    11. }
    The actual mesh generation is broken up into 2 systems,

    First system, MeshBuildSystem has 2 jobs that run after each other.

    First Job: GetVisibleFacesJob
    This job iterates all Chunk.Blocks and checks when a block is not touching another block (therefore should generate a face) and stores this in the ChunkMesh.Faces array. Basically just doing this:

    Code (CSharp):
    1. if (x >= 1) left = Blocks[GetIndex(x - 1, y, z)] != 0;
    2. if (y >= 1) bottom = Blocks[GetIndex(x, y - 1, z)] != 0;
    3. if (z >= 1) front = Blocks[GetIndex(x, y, z - 1)] != 0;
    4. if (x < Bounds.x - 1) right = Blocks[GetIndex(x + 1, y, z)] != 0;
    5. if (y < Bounds.y - 1) top = Blocks[GetIndex(x, y + 1, z)] != 0;
    6. if (z < Bounds.z - 1) back = Blocks[GetIndex(x, y, z + 1)] != 0;
    Second Job: GenerateMeshJob
    This generates my actual meshes using a cubemodel which I just map the faces to.

    This code will look a little weird because I'm doing greedy meshing but should give you an idea.

    Code (CSharp):
    1. for (var n = 0; n < 4; n++)
    2. {                  
    3.     // Our vertex model is specifically organized so that it'll work on any face
    4.     var vertexIndex = face * 4 + n;
    5.     var vertices = ModelVertices[vertexIndex];
    6.     vertices.x += start.x + (n == 2 || n == 3 ? dif.x : 0);
    7.     vertices.y += start.y + (n == 1 || n == 2 ? dif.y : 0);
    8.     vertices.z += start.z + (n == 1 || n == 3 ? dif.z : 0);
    9.     Vertices[vertCount + n] = vertices;
    10.    
    11.     Vector3 uv = ModelUVs[vertexIndex];
    12.     uv.x *= 1 + uvX;
    13.     uv.y *= 1 + uvY;
    14.     uv.z = texture;
    15.    
    16.     UVs[vertCount + n] = uv;
    17.    
    18.     Normals[vertCount + n] = ModelNormals[face];
    19. }
    20.  
    21. for (var n = 0; n < 6; n++)
    22.     Indices[trisCount + n] = vertCount + ModelIndices[face * 6 + n];
    23.  
    24. // ...
    25.  
    26.     public struct CubeModel : IDisposable
    27.     {
    28.         public NativeArray<Vector3> Vertices;
    29.         public NativeArray<Vector3> Normals;
    30.         public NativeArray<Vector2> Uvs;
    31.         public NativeArray<int> Indices;
    32.  
    33.         public static CubeModel Create()
    34.         {
    35.             return new CubeModel
    36.             {
    37.                 Vertices = new NativeArray<Vector3>(new[]
    38.                     {
    39.                         new Vector3(0f, 0f, 0f), new Vector3(0f, 1f, 1f),
    40.                         new Vector3(0f, 1f, 0f), new Vector3(0f, 0f, 1f),
    41.  
    42.                         new Vector3(1f, 0f, 0f), new Vector3(1f, 1f, 1f),
    43.                         new Vector3(1f, 1f, 0f), new Vector3(1f, 0f, 1f),
    44.  
    45.                         new Vector3(0f, 1f, 0f), new Vector3(0f, 1f, 1f),
    46.                         new Vector3(1f, 1f, 0f), new Vector3(1f, 1f, 1f),
    47.  
    48.                         new Vector3(0f, 0f, 0f), new Vector3(0f, 0f, 1f),
    49.                         new Vector3(1f, 0f, 0f), new Vector3(1f, 0f, 1f),
    50.  
    51.                         new Vector3(0f, 0f, 0f), new Vector3(0f, 1f, 0f),
    52.                         new Vector3(1f, 1f, 0f), new Vector3(1f, 0f, 0f),
    53.  
    54.                         new Vector3(0f, 0f, 1f), new Vector3(0f, 1f, 1f),
    55.                         new Vector3(1f, 1f, 1f), new Vector3(1f, 0f, 1f)
    56.                     },
    57.                     Allocator.Persistent),
    58.  
    59.                 Normals = new NativeArray<Vector3>(new[]
    60.                     {
    61.                         new Vector3(-1, 0, 0),
    62.                         new Vector3(+1, 0, 0),
    63.                         new Vector3(0, +1, 0),
    64.                         new Vector3(0, -1, 0),
    65.                         new Vector3(0, 0, -1),
    66.                         new Vector3(0, 0, +1)
    67.                     },
    68.                     Allocator.Persistent),
    69.          
    70.                 Uvs = new NativeArray<Vector2>(new[]
    71.                 {
    72.                     new Vector2(1f, 0f), new Vector2(0f, 1f),
    73.                     new Vector2(1f, 1f), new Vector2(0f, 0f),
    74.  
    75.                     new Vector2(0f, 0f), new Vector2(1f, 1f),
    76.                     new Vector2(0f, 1f), new Vector2(1f, 0f),
    77.                
    78.                     new Vector2(0f, 0f), new Vector2(0f, 1f),
    79.                     new Vector2(1f, 0f), new Vector2(1f, 1f),
    80.  
    81.                     new Vector2(0f, 0f), new Vector2(0f, 1f),
    82.                     new Vector2(1f, 0f), new Vector2(1f, 1f),
    83.  
    84.                     new Vector2(0f, 0f), new Vector2(0f, 1f),
    85.                     new Vector2(1f, 1f), new Vector2(1f, 0f),
    86.  
    87.                     new Vector2(1f, 0f), new Vector2(1f, 1f),
    88.                     new Vector2(0f, 1f), new Vector2(0f, 0f)
    89.                 }, Allocator.Persistent),
    90.  
    91.                 Indices = new NativeArray<int>(new[]
    92.                     {
    93.                         0, 1, 2, 0, 3, 1, // left -
    94.                         0, 1, 3, 0, 2, 1, // right +
    95.                         0, 3, 2, 0, 1, 3, // top +
    96.                         0, 3, 1, 0, 2, 3, // bottom -
    97.                         0, 2, 3, 0, 1, 2, // front +
    98.                         0, 2, 1, 0, 3, 2 // back -
    99.                     },
    100.                     Allocator.Persistent)
    101.             };
    102.         }
    103.     }
    My algorithm is a bit more complicated though because I implemented greedy meshing, but mapping each face to 4 verts, 4 uvs, etc is pretty easy.

    The second system is MeshApplySystem
    This is pretty basic, just loops all the chunks and applies them to meshes and is the only part that isn't executed in a job

    I have a separate MeshFaceCullingSystem but that doesn't change the mesh generation at all, just tells the MeshApplySystem to apply the new mesh - still uses the same data previously generated by the MeshBuildSystem.

    I keep memory usage down by only generating meshes that around the camera and destroying them when they're out of range. This is why Chunk and ChunkMesh are separate components as I don't have meshes at all times, but I have all chunks loaded at all times (because I do off screen pathfinding etc) and I have a system that creates the destroys the required ChunkMeshes in vision.

    I do permanently store the mesh data for each chunk (within range). So memory costs aren't cheap, but it's fine for my game. I do this so I can cull backfaces very quickly with pretty much no cost (I don't need to regenerate meshes.)
    Because I don't know how long each set of UVs, Verts etc are I use some custom containers I wrote that are basically wrappers for List<T>. You can look at them here: https://github.com/tertle/Unsafe

    Code (CSharp):
    1. public class MeshFaces : IDisposable
    2. {
    3.     public MeshFace Left { get; }
    4.     public MeshFace Right { get; }
    5.     public MeshFace Top { get; }
    6.     public MeshFace Bottom { get; }
    7.     public MeshFace Front { get; }
    8.     public MeshFace Back { get; }
    9.  
    10.     ...
    11.  
    12. }
    13.  
    14. public class MeshFace : IDisposable
    15. {
    16.     public readonly FixedList<Vector3> Vertices;
    17.     public readonly FixedList<Vector3> Normals;
    18.     public readonly FixedList<Vector3> UVs;
    19.     public readonly FixedList<int> Indices;
    20.     public NativeCounter Counter;
    21.  
    22.     public MeshFace(int maxfaces)
    23.     {
    24.         Vertices = new FixedList<Vector3>(maxfaces * 4);
    25.         Normals = new FixedList<Vector3>(maxfaces * 4);
    26.         UVs = new FixedList<Vector3>(maxfaces * 4);
    27.         Indices = new FixedList<int>(maxfaces * 6);
    28.  
    29.         Counter = new NativeCounter(Allocator.Persistent);
    30.     }
    31.  
    32.     public void Resize()
    33.     {
    34.         var writeCount = Counter.Count;
    35.         var vertexCount = writeCount * 4;
    36.         var indicesCount = writeCount * 6;
    37.  
    38.         Vertices.Resize(vertexCount);
    39.         Normals.Resize(vertexCount);
    40.         UVs.Resize(vertexCount);
    41.         Indices.Resize(indicesCount);
    42.     }
    43.  
    44.     public void Dispose()
    45.     {
    46.         if(Counter.IsCreated) Counter.Dispose();
    47.         Vertices?.Dispose();
    48.         UVs?.Dispose();
    49.         Normals?.Dispose();
    50.         Indices?.Dispose();
    51.     }
    52. }
    Let me know if there's anything you're unclear of (I'm not a great explainer) or you have any other questions.

    -edit-

    I should add, if you want to create a really large world with lots of chunks or targeting non desktop devices with lower memory limitations, you won't be able to permanently store the generated mesh the memory costs are too high. You'll have to look at doing what I originally did and just storing like 8 copies that are reused and passed job to job when generating a mesh.
     
    bb8_1 likes this.
  15. tobischw

    tobischw

    Joined:
    Aug 28, 2012
    Posts:
    21
    Hi,

    I know it's been a while since this post, I am curious how you are actually generating the mesh. I managed to re-create the GetVisibleFaceJobs, but how are the meshes actually being generated and applied to the mesh? Are you using a ParallelForJob, for each face?

    You said that "mapping each face to 4 verts, 4 uvs, etc is pretty easy." Why 4 verts exactly?

    Thanks for any clarification.
     
  16. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Got a demo here I put together a few weeks ago to help people. This particular class applies the arrays to the actual mesh.

    https://github.com/tertle/MeshDemo/blob/master/Assets/Scripts/Systems/Mesh/MeshSystem.cs

    Because each side of a cube has 4 verts which is what that code was generating.
     
    bb8_1 likes this.
  17. tobischw

    tobischw

    Joined:
    Aug 28, 2012
    Posts:
    21
    Oh, thanks, that looks great! :)

    If you don't mind me asking some more questions, I am trying to understand some of your code snippets because I think ECS/Jobs for voxel terrains is a great idea, and you seem to have a really good grasp on it. I have written a voxel engine using "classical" Unity before, but my mesh generation was horribly inefficient.

    I get the purpose of BlockFaces (in my case, just a bunch of boolean ints that store if the face gets rendered, ints so that it's blittable), but how does MeshFaces get involved? Does it store the mesh for a specific side (e.g. all Left side block meshes) in one chunk? Where do you read the Faces array and create/insert the MeshFaces?

    I'm not too worried about any of the chunking systems yet since I just want to see if I can get a simple, one chunk mesh rendered using some of your code.

    Thanks for your help.
     
  18. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    Just had a re-read of my code because I could not remember.
    It's been refactored a lot since this was posted and you shouldn't do it this way.
    DynamicBuffers have been released since then and they are much better containers for holding mesh data.

    I might try post some updated code at some point, maybe not till start of new year though.
     
    oldhighscore, bb8_1, SugoiDev and 2 others like this.
  19. tobischw

    tobischw

    Joined:
    Aug 28, 2012
    Posts:
    21
    Ah, that clears it up. I (and I am sure many others) would really appreciate that, thanks.
     
  20. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    The demo above creates a cube and manipulates the mesh every frame. It's a good starting point.
     
    tobischw likes this.
  21. tobischw

    tobischw

    Joined:
    Aug 28, 2012
    Posts:
    21
    I started to work on this somewhat. Would you recommend a hybrid ECS/classic system (i.e. a chunk is still a GameObject but with ECS components), or a pure ECS approach (using a bootstrap class or similar)? I haven't found a lot of info about the benefits of either, considering ECS is still very experimental.
     
    Last edited: Jan 1, 2019
  22. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    bb8_1, castana1962 and tobischw like this.
  23. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,642
    Mesh SetVertices and similar functions used to be slow, I must dig in the Unity ECS code to see if they still use Mesh classes. Do you have any clue?
     
  24. castana1962

    castana1962

    Joined:
    Apr 10, 2013
    Posts:
    400
    Hi tertle.
    Sorry for my ignorance but I am trying to simplifying the meshes using DOTS because I work with VR projects and I need to optimize my 3D Assets for VR purposes(similar Mobile developing)
    I was looking for any info about this topic but I only found that I would have to create IJob type that reduce the overhead when working with meshes but to be honest with you I have no idea for where I start, for it, Do you think if your MeshSystem.cs script could help me to do it? or Could you advice or help me to make i? Would it be possible?
    Hopefully you can understand me
    Thanks for your time and sorry for my little English
    Best Regards
    Alejandro
     
  25. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,759
    MeshSystem is the final step of my mesh manipulation. It takes the mesh data you've manipulated in a job and then applies it to a mesh. Do note that code will work, but you should probably be updated to the new ForEach api.

    The actual demo shows a quick mesh manipulation in jobs, again however this demo has not been updated to last version of the entity package.
     
    bb8_1 and castana1962 like this.
  26. castana1962

    castana1962

    Joined:
    Apr 10, 2013
    Posts:
    400
    Hi tertle
    Thanks for your advice
    Best Regards
    Alejandro
     
  27. mohydineName

    mohydineName

    Joined:
    Aug 30, 2009
    Posts:
    301
    Hi Tertle,

    Would you mind sharing your UnsafeNativeArray implementation? I can't find it anywhere.

    Thanks

    Stephane
     
    MostHated likes this.
  28. Nyanpas

    Nyanpas

    Joined:
    Dec 29, 2016
    Posts:
    406
    I'd also like to see how close/low level you can go with this. :3c
     
    MostHated and learc83 like this.