• Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
  • Asset Store
  • Get Unity

UNITY ACCOUNT

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account
  • Blog
  • Forums
  • Answers
  • Evangelists
  • User Groups
  • Beta Program
  • Advisory Panel

Navigation

  • Home
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
    • Blog
    • Forums
    • Answers
    • Evangelists
    • User Groups
    • Beta Program
    • Advisory Panel

Unity account

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account

Language

  • Chinese
  • Spanish
  • Japanese
  • Korean
  • Portuguese
  • Ask a question
  • Spaces
    • Default
    • Help Room
    • META
    • Moderators
    • Topics
    • Questions
    • Users
    • Badges
  • Home /
avatar image
0
Question by oldhighscore · Dec 02, 2020 at 12:33 AM · dots

Need help refactoring a simple mesh heightfield generator w/ proper use of DOTS stack

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.

[code=CSharp] 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;
     }
 }

} [/code]

And here is where I'm currently left off scratching my head in the refactor.

[code=CSharp] 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);
                 }
             });[/code]
Comment
Add comment
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users

1 Reply

· Add your reply
  • Sort: 
avatar image
0

Answer by andrew-lukasik · Dec 04, 2020 at 10:35 PM

I've had similar project some time ago and here is what I come up with (more of less). Preview image shows 900 meshes with no seams between them (rather important).

preview


NOTE: RenderMesh is part of com.unity.rendering.hybrid package.

 // src: https://gist.github.com/andrew-raphael-lukasik/104e5826015c2f4d47021ba564e9bfe8
 // REQUIRES: com.unity.rendering.hybrid package
 using System.Runtime.CompilerServices;
 using System.Collections.Generic;
 using UnityEngine;
 using Unity.Mathematics;
 using Unity.Entities;
 using Unity.Rendering;
 using Unity.Collections;
 using Unity.Transforms;
 using Unity.Jobs;
 
 public class NoiseFieldAuthoring : MonoBehaviour
 {
     [SerializeField] Material _material = null;
     [SerializeField] int2 _cells = new int2{ x=3 , y=5 };
     [SerializeField] float3 _size = new float3{ x=100 , y=0.1f , z=100 };
     [SerializeField][Range(2,256)] int _subdivision = 10;
     [SerializeField] float2 _noiseOrigin = 0;
     void OnEnable ()
     {
         var commander = World.DefaultGameObjectInjectionWorld.EntityManager;
         var archetype = commander.CreateArchetype(
                 typeof(NoiseFieldSegment)
             ,    typeof(LocalToWorld)
             ,    typeof(RenderMesh)
             ,    typeof(RenderBounds)
             ,    typeof(WorldRenderBounds)
             ,    ComponentType.ChunkComponent<ChunkWorldRenderBounds>()
 
             ,    typeof(DIRTY)
         );
 
         float3 transformPosition = transform.position;
         for( int x=0 ; x<_cells.x ; x++ )
         for( int y=0 ; y<_cells.y ; y++ )
         {
             float3 cellPos = new float3{ x=x*_size.x/_subdivision , z=y*_size.z/_subdivision };
             float3 worldPosition = transformPosition + cellPos;
             Entity entity = commander.CreateEntity( archetype );
             commander.SetComponentData( entity , new NoiseFieldSegment{
                 NoiseOrigin        = _noiseOrigin + new float2{ x=cellPos.x , y=cellPos.z } ,
                 Subdivision        = _subdivision ,
                 Size            = _size ,
             } );
             var mesh = new Mesh();
             mesh.name = $"mesh {entity}";
             commander.SetSharedComponentData( entity , new RenderMesh{
                 mesh        = mesh ,
                 material    = _material
             } );
             commander.SetComponentData( entity , new LocalToWorld{
                 Value = float4x4.Translate( worldPosition )
             } );
             commander.SetComponentData<RenderBounds>( entity , new RenderBounds{ Value = new AABB{ Center=_size/2f , Extents=_size } } );
         }
     }
 }
 
 public struct DIRTY : IComponentData {}
 public struct NoiseFieldSegment : IComponentData
 {
     public float2 NoiseOrigin;
     public int Subdivision;
     public float3 Size;
 }
 
 [UpdateInGroup( typeof(InitializationSystemGroup) )]
 public class NoiseFieldSegmentSystem : SystemBase
 {
     EntityQuery _query;
     EndSimulationEntityCommandBufferSystem _endSimulationEcbSystem;
     NativeList<JobHandle> batches = new NativeList<JobHandle>( Allocator.Persistent );
     Queue<(Mesh mesh, NativeArray<float3> vertices,NativeArray<float3>normals,NativeArray<int> indices)> _output = new Queue<(Mesh,NativeArray<float3>,NativeArray<float3>,NativeArray<int>)>( 10 );
     protected override void OnCreate ()
     {
         _query = EntityManager.CreateEntityQuery(
                 ComponentType.ReadOnly<NoiseFieldSegment>()
             ,    ComponentType.ReadOnly<RenderMesh>()
             ,    ComponentType.ReadOnly<DIRTY>()
         );
         _endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
     }
     protected override void OnDestroy ()
     {
         if( batches.IsCreated ) batches.Dispose();
         Dependency.Complete();
         while( _output.Count!=0 )
         {
             var output = _output.Dequeue();
             output.vertices.Dispose();
             output.indices.Dispose();
             output.normals.Dispose();
         }
     }
     protected override void OnUpdate ()
     {
         // apply work results:
         while( _output.Count!=0 )
         {
             var output = _output.Dequeue();
             var mesh = output.mesh;
             mesh.SetVertices( output.vertices );
             mesh.SetIndices( output.indices , MeshTopology.Triangles , 0 );
             mesh.SetNormals( output.normals );
             output.vertices.Dispose();
             output.indices.Dispose();
             output.normals.Dispose();
 
             #if DEBUG
             Debug.Log($"{mesh.name} - updated");
             #endif
         }
 
         // schedule work:
         if( _query.CalculateEntityCount()!=0 )
         {
             var cmd = _endSimulationEcbSystem.CreateCommandBuffer();
             var entities = _query.ToEntityArray( Allocator.Temp );
             var fieldData = GetComponentDataFromEntity<NoiseFieldSegment>( isReadOnly:true );
             foreach( Entity entity in entities.Slice() )
             {
                 var mesh = EntityManager.GetSharedComponentData<RenderMesh>( entity ).mesh;
                 var fieldSegment = fieldData[ entity ];
                 int subdivision = fieldSegment.Subdivision;
                 float3 scale = fieldSegment.Size / new float3{ x=subdivision-1 , y=1 , z=subdivision-1 };
                 float2 noiseOrigin = fieldSegment.NoiseOrigin;
 
                 int numVertices = subdivision*subdivision;
                 int numTriangles = (subdivision-1)*(subdivision-1)*2;
                 int numIndices = numTriangles*3;
                 int numNormals = numVertices;
                 NativeArray<float3> vertices = new NativeArray<float3>( numVertices , Allocator.TempJob , NativeArrayOptions.UninitializedMemory );
                 NativeArray<float3> normals = new NativeArray<float3>( numNormals , Allocator.TempJob , NativeArrayOptions.ClearMemory );
                 NativeArray<int> indices = new NativeArray<int>( numIndices , Allocator.TempJob , NativeArrayOptions.UninitializedMemory );
                 
                 var verticesJobHandle =  new VerticesJob{
                     Width            = subdivision ,
                     Scale            = scale ,
                     NoiseOrigin        = noiseOrigin ,
                     Vertices        = vertices
                 }.Schedule( numVertices , 32 );
                 var trianglesJobHandle = new TrianglesJob{
                     Width        = subdivision ,
                     Indices        = indices
                 }.Schedule();
                 var normalsJobHandle = new NormalsJob{
                     Width            = subdivision ,
                     Scale            = scale ,
                     NoiseOrigin        = noiseOrigin ,
                     Normals            = normals
                 }.Schedule( numNormals , 32 , JobHandle.CombineDependencies(verticesJobHandle,trianglesJobHandle) );
 
                 batches.Add( JobHandle.CombineDependencies( verticesJobHandle , normalsJobHandle ) );
                 _output.Enqueue( ( mesh:mesh , vertices:vertices , normals:normals , indices:indices ) );
                 cmd.RemoveComponent<DIRTY>( entity );
 
                 #if DEBUG
                 Debug.Log($"{mesh.name} - jobs scheduled");
                 #endif
             }
             entities.Dispose();
             Dependency = JobHandle.CombineDependencies( batches );
             batches.Clear();
 
             _endSimulationEcbSystem.AddJobHandleForProducer( Dependency );
         }
     }
 }
 
 [Unity.Burst.BurstCompile]
 public struct VerticesJob : IJobParallelFor
 {
     public int Width;
     public float3 Scale;
     public float2 NoiseOrigin;
     [WriteOnly] public NativeArray<float3> Vertices;
     void IJobParallelFor.Execute ( int index )
     {
         Vertices[index] = Vertex(
             index2:            Index1dTo2d(index,Width) ,
             widthHeight:    new int2{ x=Width , y=Width } ,
             areaScale:        Scale ,
             noiseOrigin:    NoiseOrigin
         );
     }
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public static float3 Vertex ( int2 index2 , int2 widthHeight , float3 areaScale , float2 noiseOrigin )
     {
         float2 xy = (float2)index2 / (float2)widthHeight * new float2{ x=areaScale.x , y=areaScale.z };
         float height = noise.snoise( xy + noiseOrigin );
         return new float3{ x=xy.x , y=height*areaScale.y , z=xy.y };
     }
 
     [MethodImpl(MethodImplOptions.AggressiveInlining)]
     public static int2 Index1dTo2d ( int index , int width ) => new int2{ x=index%width , y=index/width };
 }
 
 [Unity.Burst.BurstCompile]
 public struct TrianglesJob : IJob
 {
     public int Width;
     [NativeDisableParallelForRestriction][WriteOnly] public NativeArray<int> Indices;
     void IJob.Execute ()
     {
         int width = Width, height = Width;
         int numQuadRows = width - 1, numQuadCols = height - 1;
         for( int Y=0, triangleIndex=0 ; Y<numQuadRows ; Y++ )
         for( int X=0 ; X<numQuadCols ; X++)
         {
             int topLeft = ((Y + 1) * width) + X;
             int topRight = ((Y + 1) * width) + (X + 1);
             int bottomLeft = (Y * width) + X;
             int bottomRight = (Y * width) + (X + 1);
             
             Indices[triangleIndex++] = topLeft;
             Indices[triangleIndex++] = topRight;
             Indices[triangleIndex++] = bottomLeft;
 
             Indices[triangleIndex++] = topRight;
             Indices[triangleIndex++] = bottomRight;
             Indices[triangleIndex++] = bottomLeft;
         }
     }
 }
 
 [Unity.Burst.BurstCompile]
 public struct NormalsJob : IJobParallelFor
 {
     public int Width;
     public float3 Scale;
     public float2 NoiseOrigin;
     [WriteOnly] public NativeArray<float3> Normals;
     void IJobParallelFor.Execute ( int index )
     {
         Normals[index] = NoiseFieldNormal( VerticesJob.Index1dTo2d( index , Width ) );
     }
     float3 NoiseFieldNormal ( int2 i2 )
     {
         int2 i2x1 = i2 + new int2{ x=-1 };
         int2 i2x2 = i2 + new int2{ x=1 };
         int2 i2y1 = i2 + new int2{ y=1 };
         int2 i2y2 = i2 + new int2{ y=-1 };
 
         float3 vx1 = VerticesJob.Vertex(
             index2:            i2x1 ,
             widthHeight:    new int2{ x=Width , y=Width } ,
             areaScale:        Scale ,
             noiseOrigin:    NoiseOrigin
         );
         float3 vx2 = VerticesJob.Vertex(
             index2:            i2x2 ,
             widthHeight:    new int2{ x=Width , y=Width } ,
             areaScale:        Scale ,
             noiseOrigin:    NoiseOrigin
         );
         float3 vy1 = VerticesJob.Vertex(
             index2:            i2y1 ,
             widthHeight:    new int2{ x=Width , y=Width } ,
             areaScale:        Scale ,
             noiseOrigin:    NoiseOrigin
         );
         float3 vy2 = VerticesJob.Vertex(
             index2:            i2y2 ,
             widthHeight:    new int2{ x=Width , y=Width } ,
             areaScale:        Scale ,
             noiseOrigin:    NoiseOrigin
         );
         float3 vx = math.normalize( vx2 - vx1 );
         float3 vy = math.normalize( vy2 - vy1 );
         return math.cross( vx , vy );
     }
 }

Comment
Add comment · Show 1 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image andrew-lukasik · Dec 04, 2020 at 10:46 PM 0
Share

old answer

Keep in $$anonymous$$d that Unity.Entities alone do not produce better performance automagically. Consider Unity.Jobs first. This solution is simple and fast:


SOURCE CODE moved here


preview

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Welcome to Unity Answers

If you’re new to Unity Answers, please check our User Guide to help you navigate through our website and refer to our FAQ for more information.

Before posting, make sure to check out our Knowledge Base for commonly asked Unity questions.

Check our Moderator Guidelines if you’re a new moderator and want to work together in an effort to improve Unity Answers and support our users.

Follow this Question

Answers Answers and Comments

140 People are following this question.

avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image

Related Questions

Best way to get data from a Monobheaviour in a JobComponentSystem? 2 Answers

[ECS] DefaultWorldInitialization.Initialize take 50MB 1 Answer

Unity (ECS & DOTS) Entity Sinking 0 Answers

How to create realistic wire 0 Answers

Changing mesh collider's mesh cause objects go into mesh. 1 Answer


Enterprise
Social Q&A

Social
Subscribe on YouTube social-youtube Follow on LinkedIn social-linkedin Follow on Twitter social-twitter Follow on Facebook social-facebook Follow on Instagram social-instagram

Footer

  • Purchase
    • Products
    • Subscription
    • Asset Store
    • Unity Gear
    • Resellers
  • Education
    • Students
    • Educators
    • Certification
    • Learn
    • Center of Excellence
  • Download
    • Unity
    • Beta Program
  • Unity Labs
    • Labs
    • Publications
  • Resources
    • Learn platform
    • Community
    • Documentation
    • Unity QA
    • FAQ
    • Services Status
    • Connect
  • About Unity
    • About Us
    • Blog
    • Events
    • Careers
    • Contact
    • Press
    • Partners
    • Affiliates
    • Security
Copyright © 2020 Unity Technologies
  • Legal
  • Privacy Policy
  • Cookies
  • Do Not Sell My Personal Information
  • Cookies Settings
"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
  • Anonymous
  • Sign in
  • Create
  • Ask a question
  • Spaces
  • Default
  • Help Room
  • META
  • Moderators
  • Explore
  • Topics
  • Questions
  • Users
  • Badges