Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Fastest way to create a MeshCollider during conversion

Discussion in 'Entity Component System' started by TLRMatthew, Mar 13, 2020.

  1. TLRMatthew

    TLRMatthew

    Joined:
    Apr 10, 2019
    Posts:
    65
    I am converting some of my older code over to using the GameObject conversion workflow as a way to learn it. One of those parts of code would, in Awake, create a custom mesh from heightmap data, and then create a (legacy) MeshCollider using that Mesh.

    Now I'd like to achieve the same result but using a GameObjectConversionSystem and a Unity.Physics.MeshCollider, since that is now fast enough to use even for large meshes when Bursted.

    I've got it working, but I don't really think I'm doing it the "right" way.

    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Jobs;
    3. using Unity.Mathematics;
    4. using Unity.Collections;
    5. using Unity.Entities;
    6. using Unity.Physics;
    7. using Unity.Physics.Authoring;
    8. using Unity.Rendering;
    9.  
    10. using Color = UnityEngine.Color;
    11. using Color32 = UnityEngine.Color32;
    12. using Mesh = UnityEngine.Mesh;
    13. using Material = UnityEngine.Material;
    14.  
    15. public class MountainConversion : GameObjectConversionSystem
    16. {
    17.     private struct ColliderData
    18.     {
    19.         public NativeArray<float3> Verts;
    20.         public NativeList<int3> Triangles;
    21.  
    22.         public static ColliderData Create()
    23.         {
    24.             ColliderData result = new ColliderData();
    25.             result.Triangles = new NativeList<int3>(Allocator.TempJob);
    26.  
    27.             return result;
    28.         }
    29.  
    30.         public void Dispose()
    31.         {
    32.             Verts.Dispose();
    33.             Triangles.Dispose();
    34.         }
    35.     }
    36.  
    37.     private struct BuildData
    38.     {
    39.         public int NextVertexIndex;
    40.         public UnityEngine.Vector3[] Vertices;
    41.         public UnityEngine.Vector2[] UVs;
    42.         public UnityEngine.Vector2[] NormUVs;
    43.  
    44.         public NativeList<int> Triangles;
    45.         public float MinVertY;
    46.         public float MaxVertY;
    47.  
    48.         public ColliderData ColliderData;
    49.  
    50.         public static BuildData Create()
    51.         {
    52.             BuildData result = new BuildData();
    53.             result.MinVertY = float.MaxValue;
    54.             result.MaxVertY = float.MinValue;
    55.             result.Triangles = new NativeList<int>(Allocator.Temp);
    56.             result.ColliderData = ColliderData.Create();
    57.  
    58.             return result;
    59.         }
    60.  
    61.         public void Dispose()
    62.         {
    63.             Triangles.Dispose();
    64.             ColliderData.Dispose();
    65.         }
    66.     }
    67.  
    68.     private BuildData CreateBuildData(Heightmap heightmap, int sampleRate, Mountain.ShadingMode shadingMode)
    69.     {
    70.         BuildData buildData = BuildData.Create();
    71.  
    72.         // lots of stuff...
    73.  
    74.         return buildData;
    75.     }
    76.  
    77.     private void AddMesh(Entity entity, BuildData buildData, Material mountainMaterial)
    78.     {
    79.         // Create the mesh
    80.         Mesh mountainMesh = new Mesh();
    81.  
    82.         // If we have more than ~65k verts then we need to use a UInt32 for indexing them
    83.         if (buildData.Vertices.Length > ushort.MaxValue)
    84.         {
    85.             mountainMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
    86.         }
    87.  
    88.         mountainMesh.vertices = buildData.Vertices;
    89.         mountainMesh.triangles = buildData.Triangles.ToArray();
    90.  
    91.         // UVs
    92.         // uv contains repeating UV data
    93.         // uv2 contains normalised normal data (eg. u and v are both in the range 0-1)
    94.         mountainMesh.uv = buildData.UVs;
    95.         mountainMesh.uv2 = buildData.NormUVs;
    96.  
    97.         // Debug vertex colours:
    98.         Color32[] colors = new Color32[buildData.Vertices.Length];
    99.         for (int i = 0; i < buildData.Vertices.Length; i++)
    100.         {
    101.             float t = (buildData.Vertices[i].y - buildData.MinVertY) / (buildData.MaxVertY - buildData.MinVertY);
    102.             colors[i] = Color32.Lerp(Color.red, Color.green, t);
    103.         }
    104.         mountainMesh.colors32 = colors;
    105.  
    106.         // make Unity do a "best guess" at the Normals for the mountain.
    107.         // shared vertices will result in a "smooth" surface, while doubled-up vertices create an "edge"
    108.         mountainMesh.RecalculateNormals();
    109.  
    110.         DstEntityManager.AddSharedComponentData<RenderMesh>(entity, new RenderMesh
    111.         {
    112.             mesh = mountainMesh,
    113.             material = mountainMaterial
    114.         });
    115.  
    116.         DstEntityManager.AddComponentData<RenderBounds>(entity, new RenderBounds
    117.         {
    118.             Value = mountainMesh.bounds.ToAABB()
    119.         });
    120.     }
    121.  
    122.     [BurstCompile]
    123.     struct CreateColliderJob : IJob
    124.     {
    125.         [ReadOnly] public ColliderData ColliderData;
    126.         [ReadOnly] public PhysicsCategoryTags CollisionLayer;
    127.  
    128.         public NativeArray<BlobAssetReference<Collider>> Colliders;
    129.  
    130.         public void Execute()
    131.         {
    132.             Colliders[0] = MeshCollider.Create(
    133.                 ColliderData.Verts, ColliderData.Triangles,
    134.                 new CollisionFilter
    135.                 {
    136.                     BelongsTo = CollisionLayer.Value,
    137.                     CollidesWith = CollisionLayer.Value,
    138.                     GroupIndex = 0
    139.                 }
    140.             );
    141.         }
    142.     }
    143.  
    144.     protected override void OnUpdate()
    145.     {
    146.         Entities.ForEach((Mountain mountain) =>
    147.         {
    148.             Entity entity = GetPrimaryEntity(mountain);
    149.             BuildData buildData = CreateBuildData(mountain.Heightmap, mountain.SampleRate, mountain.Shading);
    150.             AddMesh(entity, buildData, mountain.MountainMaterial);
    151.  
    152.             var colliderJob = new CreateColliderJob
    153.             {
    154.                 ColliderData = buildData.ColliderData,
    155.                 CollisionLayer = mountain.CollisionLayer,
    156.                 Colliders = new NativeArray<BlobAssetReference<Collider>>(1, Allocator.TempJob)
    157.             };
    158.             colliderJob.Run();
    159.             DstEntityManager.AddComponentData<PhysicsCollider>(entity, new PhysicsCollider { Value = colliderJob.Colliders[0] });
    160.             colliderJob.Colliders.Dispose();
    161.  
    162.             buildData.Dispose();
    163.         });
    164.     }
    165. }
    166.  
    A few notes/questions:
    ColliderData is separate because it contains only blittable data, as I need to pass it to the Collider job. Is there a way to create a Mesh without using the UnityEngine vector types? At the moment I have to double-handle everything in the code I've omitted, adding it once to the job-friendly CollisionData and once to the UnityEngine-friendly BuildData.

    GameObjectConversionSystem doesn't derive from SystemBase - is there a reason for this, or is the SystemBase work still in-progress?

    Related to the above, is it expected that conversion systems will always run synchronously? Since there's no dependency fields or other ways to pass JobHandles around.

    And my main issue with my approach here: surely there's a better way to deal with the MeshCollider creation in the job. Either a way to assign it inside the job so that I don't have to pass it out (are EntityCommandBuffers ok to use in the conversion world?), or a better way to pass it out that doesn't involve using a one-element NativeArray.

    Any advice would be greatly appreciated!
     
  2. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Create/bake it at design time, and then you don't care how long it takes. Either using sub scenes or binary serialization api's directly. It's pretty much a do that or don't bother sort of thing IMO. The time you spend optimizing creation for one use case, you could have design time working, and then you have it for all.
     
    MNNoxMortem likes this.
  3. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    Also you mentioned height map data. You should take a look at the terrain collider. Height data just plugs right into it and it's optimized for that use case.
     
  4. TLRMatthew

    TLRMatthew

    Joined:
    Apr 10, 2019
    Posts:
    65
    One thing I'm going to support eventually is randomly generated mountains - I don't need them to be created in a single frame or anything, there'll be a loading screen, but I do expect to need to do this at runtime. For the preset mountains I hope to use this same approach but saving them as a subscene, yes.

    The approach I'm using above is fast enough for my purposes right now, I'm more concerned about learning the correct patterns. I've just found this reference to 2020.1 Mesh API improvements that answers at least how I can hopefully get the Mesh creation out of UnityEngine land, but I'm still concerned about how I'm getting the Collider out of the CreateColliderJob.
     
  5. TLRMatthew

    TLRMatthew

    Joined:
    Apr 10, 2019
    Posts:
    65
    I had looked at the terrain stuff some time ago and figured it wasn't quite ready yet, but I'll have another look. Definitely still interested in the right approach to what I've done above though :)
     
  6. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    We have been using the terrain colliders for several months in a fairly complex game with a bunch of different types of queries running against it without any issues. We have runtime updates since our game has terra forming. We partition the collider and design time bake plus runtime updates to specific partitions as needed.
     
    MNNoxMortem likes this.