Convert Terrain Details and Trees to DOTS Entities

Hello Unity DOTS Devs,

I am currently trying to understand the latest DOTS as it is quite different now from the previous experimental versions from 2021. My main problem now is that I can’t convert GameObjects that easily anymore with GameObjectConversionUtility.ConvertGameObjectHierarchy, but instead need to do the Authoring and Baker process.

However, what I am currently trying to achieve is:

  1. Create Entity Prefabs on runtime. Basically, I have a list of prefab GameObjects and want to convert them to Entity prefabs. I don’t want to add all manually to the sub-scene with a EntityPrefabAuthoring.
    :heavy_check_mark: (Use DynamicBuffer instead of arrays and IBufferElementData instead of IComponentData)
  2. I want to use these Entity prefabs and instantiate entities at given position, rotation and scale without using ISystem, but a MonoBehaviour instead (EntityManager.Instantiate()).
    :heavy_check_mark: (ISystem or SystemBase is needed)
  3. The Transform, Rotation and Scale are now inside the LocalTransform struct, but while Position and Rotation work as expected, Scale has only a float and not a float3, so I think I must be missing something here.
    :heavy_check_mark: (PostTransformMatrix needs to be added after instantiation and setting LocalTransform in the ISystem)

So, I think this is pretty basic stuff, but I can’t grasp the new concept at the moment to achieve this. Is there no easy conversion like GameObjectConversionUtility.ConvertGameObjectHierarchy anymore?

Runtime conversion was completely removed in Entities 1.0. If you want to convert GameObjects to entities at runtime, you have to build your own solution for it.

Well, that’s unfortunate. Then I’ll try to build up an automatic system that’s get all baking candidates beforehand and creates EntityReferences out of it. Doing it by hand seems tedious for me.

@DreamingImLatios I figured now something different out: Is it intentional that the subscene does the GameObject conversion perfectly, but using Authoring, Baker and GetEntity seems to ignore Components like the MeshCollider? Is there a function to take advantage of the subscene conversion method as I feel like it does the job better?

Well, I better share my code. I am trying to convert terrain trees to entities, but the tree entities don’t have a MeshCollider at the end.

This script is attached to the terrain:

using UnityEditor;
using UnityEngine;

[RequireComponent(typeof(Terrain))]
public class TerrainAccess : MonoBehaviour
{
    public static Terrain terrain;
    public static TreeInstance[] originalTreeInstances;

    private void OnValidate()
    {
        if (terrain == null)
            terrain = GetComponent<Terrain>();
    }

    private void Awake()
    {
        //remember the original treeInstances
        originalTreeInstances = terrain.terrainData.treeInstances;

        //restore the trees when the scene changes or in editor exit play mode
        SceneController.instance.onSceneChange.AddListener(RestoreTrees);
#if UNITY_EDITOR
        EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
#endif
    }

#if UNITY_EDITOR
    private static void OnPlayModeStateChanged(PlayModeStateChange state)
    {
        //check if exiting play mode
        if (state == PlayModeStateChange.ExitingPlayMode)
            RestoreTrees();
    }
#endif

    public static void ClearTrees()
    {
        //clear all tree instances
        terrain.terrainData.treeInstances = new TreeInstance[0];

        //refresh the terrain immediately to see the changes
        terrain.terrainData.RefreshPrototypes();
        terrain.Flush();
    }

    public static void RestoreTrees()
    {
        //just reassign the tree instances
        terrain.terrainData.treeInstances = originalTreeInstances;

        //refresh the terrain immediately to see the changes
        terrain.terrainData.RefreshPrototypes();
        terrain.Flush();
    }
}

This script does the Authoring and Baking. It needs to be attached on a GameObject that is inside the subscene:

using System;
using System.Linq;
using Unity.Collections;
using Unity.Entities;
using UnityEngine;

[ExecuteInEditMode]
public class TerrainTreeAuthoring : MonoBehaviour
{
    public GameObject[] treePrefabs;

    //as these instance can be a lot, hide them in the inspector for better performance
    [HideInInspector] public TreeEntityInstance[] treeEntityInstances;

#if UNITY_EDITOR
    private void Update()
    {
        //don't execute this in play mode
        if (Application.isPlaying)
            return;

        //get the terrain via a static member as thread Workers have no access to Terrain.activeTerrain, GameObjects and SceneManagement
        Terrain terrain = TerrainAccess.terrain;

        //only if there is a terrain and the tree count is different, update this
        if (terrain != null && (treePrefabs == null || treePrefabs.Length != terrain.terrainData.treePrototypes.Length ||
    treeEntityInstances == null || treeEntityInstances.Length != terrain.terrainData.treeInstances.Length))
        {
            //as cross scene reference is not supported, remember only its tree data and not the terrain itself 
            //moreover, the terrain tree structs are not supported, therefore get only the needed data
            treePrefabs = terrain.terrainData.treePrototypes.Select(x => x.prefab).ToArray();
            treeEntityInstances = new TreeEntityInstance[terrain.terrainData.treeInstances.Length];

            //use span to achieve a much higher performance
            Span<TreeInstance> sourceSpan = terrain.terrainData.treeInstances.AsSpan();
            Span<TreeEntityInstance> targetSpan = treeEntityInstances.AsSpan();
            for (int i = 0; i < sourceSpan.Length; i++)
            {
                //convert TreeInstance to TreeEntityInstance
                targetSpan[i] = sourceSpan[i];

                //convert the terrain position to world position
                targetSpan[i].position = Vector3.Scale(targetSpan[i].position, terrain.terrainData.size) + terrain.GetPosition();
            }

            //set this object as dirty to save the changes
            UnityEditor.EditorUtility.SetDirty(this);
            Debug.Log("Entity trees were updated!");
        }
    }
#endif
}

public class TerrainTreeBaker : Baker<TerrainTreeAuthoring>
{
    public override void Bake(TerrainTreeAuthoring authoring)
    {
        //create an entity and add a dynamic buffer to store the prefabs
        Entity entity = GetEntity(TransformUsageFlags.None);
        DynamicBuffer<TreePrefabBuffer> prefabBuffer = AddBuffer<TreePrefabBuffer>(entity);

        //populate the buffer with terrain tree entity prefabs
        foreach (GameObject prefab in authoring.treePrefabs)
        {
            prefabBuffer.Add(new TreePrefabBuffer
            {
                prefab = GetEntity(prefab, TransformUsageFlags.Renderable)
            });
        }

        //create a BlobBuilder for the TerrainTreeBlob
        using (BlobBuilder blobBuilder = new BlobBuilder(Allocator.Temp))
        {
            ref TerrainTreeBlob root = ref blobBuilder.ConstructRoot<TerrainTreeBlob>();

            //allocate space for treeEntityInstances
            BlobBuilderArray<TreeEntityInstance> treeEntityInstances = blobBuilder.Allocate(ref root.treeEntityInstances, authoring.treeEntityInstances.Length);

            //populate the treeEntityInstances BlobArray
            for (int i = 0; i < authoring.treeEntityInstances.Length; i++)
            {
                treeEntityInstances[i] = authoring.treeEntityInstances[i];
            }

            //add the BlobAssetReference to the entity
            AddComponent(entity, new TerrainTreeData
            {
                blobData = blobBuilder.CreateBlobAssetReference<TerrainTreeBlob>(Allocator.Persistent)
            });
        }
    }
}

public struct TreePrefabBuffer : IBufferElementData
{
    public Entity prefab;
}

public struct TerrainTreeData : IComponentData
{
    public BlobAssetReference<TerrainTreeBlob> blobData;
}

public struct TerrainTreeBlob
{
    public BlobArray<TreeEntityInstance> treeEntityInstances;
}

//serializable is needed to save the struct instances properly
[Serializable]
public struct TreeEntityInstance
{
    public Vector3 position;
    public float rotation;
    public float heightScale;
    public float widthScale;
    public int prototypeIndex;

    public static implicit operator TreeEntityInstance(TreeInstance treeInstance)
    {
        return new TreeEntityInstance
        {
            position = treeInstance.position,
            widthScale = treeInstance.widthScale,
            heightScale = treeInstance.heightScale,
            rotation = treeInstance.rotation,
            prototypeIndex = treeInstance.prototypeIndex
        };
    }
}

And this System spawns the tree entities

using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

public partial struct TerrainTreeSystem : ISystem
{
    public void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<TerrainTreeData>();
    }

    public void OnUpdate(ref SystemState state)
    {
        //let the terrain clear its trees
        TerrainAccess.ClearTrees();

        //create an entity command buffer
        EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.Temp);

        //query the TerrainTreeData and its corresponding buffer
        foreach ((RefRO<TerrainTreeData> terrainTreeData, DynamicBuffer<TreePrefabBuffer> prefabBuffer, Entity entity) in
            SystemAPI.Query<RefRO<TerrainTreeData>, DynamicBuffer<TreePrefabBuffer>>().WithEntityAccess())
        {
            //get references to the BlobAsset data
            ref BlobArray<TreeEntityInstance> treeEntityInstances = ref terrainTreeData.ValueRO.blobData.Value.treeEntityInstances;

            //get all Entities that have the component with the Entity reference
            for (int i = 0; i < treeEntityInstances.Length; i++)
            {
                //instantiate the prefab Entity
                Entity instance = ecb.Instantiate(prefabBuffer[treeEntityInstances[i].prototypeIndex].prefab);

                //setup its position, rotation and scale
                ecb.SetComponent(instance, LocalTransform.FromPositionRotationScale(
                    treeEntityInstances[i].position,
                    quaternion.Euler(0, treeEntityInstances[i].rotation, 0),
                    1f
                    ));

                ecb.AddComponent(instance, new PostTransformMatrix()
                {
                    Value = float4x4.Scale(new float3(
                        treeEntityInstances[i].widthScale,
                        treeEntityInstances[i].heightScale,
                        treeEntityInstances[i].widthScale))
                });
            }

            //now remove the terrain tree data and buffer by destroying its entity as everything was instantiated
            ecb.DestroyEntity(entity);
        }

        //execute the orders and free the ecb from memory
        ecb.Playback(state.EntityManager);
        ecb.Dispose();
    }
}

EDIT 1: I updated the TerrainTreeAuthoring code and it works much better now.

  1. replaced OnValidate with Update and ExecuteInEditMode
  2. changed the condition to update the tree entities when the terrain trees have changed
  3. used Span instead of working with big arrays for much better performance (30 s → 0.002 s)
  4. added more comments

EDIT 2: I updated the TerrainTreeSystem to support non uniform scaling.

1 Like

Are you sure the prefabs have a MeshCollider? Try retrieving the MeshCollider in your baker to ensure it is there. I can’t remember off the top of my head, but I think Unity strips components off of tree and detail instances.

@DreamingImLatios Yes, I tried it already with a simple entity conversion, which was Terrain independent. If I place this GameObject directly into the subscene it converts perfectly, if I use the baking, there is no MechCollider on the entity.

I will now always update the code instead of writing a new reply.

Creating a similar system for the terrain details was much more laborious than expected. Moreover, I created an Entity Pooling System for it on the fly as well as the Entity Command Buffer was the performance bottleneck, as it had to destroy and create many details anew as soon as the camera was on another grid index and causing stuttering. Now it just takes the old ones and replaces them, which is much more efficient.

I think the following result speaks for itself: Over 190.000 trees on the whole terrain and on an area of 200x200 units there are ~3 mio details at the same time and the performance is between 30 and 60 fps, depending on where you are looking at. The GPU is the bottleneck now.

With this I can finally create a very dense and atmospheric forest.

The code is over 1.000 lines long, so I think a bit too much to post here. Therefore, I am thinking to add this as an Asset on the Unity Asset Store, but I don’t know if it should be for free or for a small amount. What do you think?

1 Like

First off, that’s really cool!

As for my opinion on the Unity Asset Store, only put it there if you intend to make money from it, and that money is sufficient incentive for you to provide good support to customers. Otherwise, the Unity Asset Store EULA is a very protective license, and assumes “free” assets are just a temporary giveaway. This limits other people’s ability to improve it and share their improvements, and it also prevents people from redistributing it in public example projects. Open-source on GitHub with a permissive license (not GPL) won’t have this problem, but if you want money, you have to resort to Patreon or similar. There’s also the path of putting it on the store first, and then if you decide you don’t want to support it anymore, making it free and putting it on GitHub.

So really which you should do should be based on what you want to get out of sharing your solution, and what you find valuable.

1 Like

@DreamingImLatios Thank you and thanks for the detailed answer.

Then I will polish it a bit further and try to sell it as an easy automatic converter for a low price first.
If there is no response to it, then I will make it for free.

On the other hand, it is very important to me that people get involved in further development so that things get even better. On the Asset Store it would mean plain Feedback. However, I have no own experience on GitHub and such. How many people will actually participate in the end and write additional useful code? That’s something I need to find out.