Search Unity

Question NavMeshSurface and Terrain Trees

Discussion in 'Navigation' started by VentaGames, Jun 14, 2022.

  1. VentaGames

    VentaGames

    Joined:
    Jul 9, 2021
    Posts:
    158
    Hi there,
    I have multiple NavMeshSurface components, one per NavMeshAgent. I have a few of them.
    But the terrain trees are ignored once I bake them. All these trees have colliders, tried mesh, and capsule colliders. Tried adding the NavMeshObstacle component to the tree prefab, but nothing helped.

    Any ideas on how I can make these trees work with NavMeshSurface?

    Huge thanks in advance.
     
  2. StaggartCreations

    StaggartCreations

    Joined:
    Feb 18, 2015
    Posts:
    2,258
    Tree colliders are a bit of black box and seem to directly tie into the physics system. Since they aren't GameObjects, nav meshes can't take them into account.

    A way around this is to create a collider object for every tree on the terrain, then baking the navmesh. You can simply delete these again afterwards. This is what I've previously used:
    Code (CSharp):
    1. using System.Linq;
    2. using UnityEngine;
    3.  
    4. [RequireComponent(typeof(Terrain))]
    5. public class ExtractTreeColliders : MonoBehaviour
    6. {
    7.     [SerializeField]
    8.     private Terrain terrain;
    9.  
    10.     private void Reset()
    11.     {
    12.         terrain = GetComponent<Terrain>();
    13.      
    14.         Extract();
    15.     }
    16.  
    17.     [ContextMenu("Extract")]
    18.     public void Extract()
    19.     {
    20.         Collider[] colliders = terrain.GetComponentsInChildren<Collider>();
    21.  
    22.         //Skip the first, since its the Terrain Collider
    23.         for (int i = 1; i < colliders.Length; i++)
    24.         {
    25.             //Delete all previously created colliders first
    26.             DestroyImmediate(colliders[i].gameObject);
    27.         }
    28.      
    29.         for (int i = 0; i < terrain.terrainData.treePrototypes.Length; i++)
    30.         {
    31.             TreePrototype tree = terrain.terrainData.treePrototypes[i];
    32.          
    33.             //Get all instances matching the prefab index
    34.             TreeInstance[] instances = terrain.terrainData.treeInstances.Where(x => x.prototypeIndex == i).ToArray();
    35.          
    36.             for (int j = 0; j < instances.Length; j++)
    37.             {
    38.                 //Un-normalize positions so they're in world-space
    39.                 instances[j].position = Vector3.Scale(instances[j].position, terrain.terrainData.size);
    40.                 instances[j].position += terrain.GetPosition();
    41.  
    42.                 //Fetch the collider from the prefab object parent
    43.                 CapsuleCollider prefabCollider = tree.prefab.GetComponent<CapsuleCollider>();
    44.                 if(!prefabCollider) continue;
    45.              
    46.                 GameObject obj = new GameObject();
    47.                 obj.name = tree.prefab.name + j;
    48.  
    49.                 CapsuleCollider objCollider = obj.AddComponent<CapsuleCollider>();
    50.  
    51.                 objCollider.center = prefabCollider.center;
    52.                 objCollider.height = prefabCollider.height;
    53.                 objCollider.radius = prefabCollider.radius;
    54.  
    55.                 if (terrain.preserveTreePrototypeLayers) obj.layer = tree.prefab.layer;
    56.                 else obj.layer = terrain.gameObject.layer;
    57.  
    58.                 obj.transform.position = instances[j].position;
    59.                 obj.transform.parent = terrain.transform;
    60.             }
    61.         }
    62.     }
    63. }
     
  3. timmccune

    timmccune

    Joined:
    Sep 27, 2018
    Posts:
    29
    Thanks for posting your work around. I wasn't able to get the above code to work correctly, so after a little modification it works like a champ. I believe that when a NavMeshSurface is baking, that it looks for a Mesh Filter, then analyses the slope and height. So I modified your code to create a primative, then used the radius as its scale. Lastly I flagged the gameobject as static. Then Attached the script to the Terrain, clicked Extract from its context menu, Then generated the NavMeshSurface, then disabled the children of the terrain gameobject. Works great now. Thanks again for your help. Below is your code with the modifications:

    Code (CSharp):
    1.  
    2. using System.Linq;
    3. using UnityEngine;
    4.  
    5. [RequireComponent(typeof(Terrain))]
    6. public class ExtractTreeCollidersFromTerrain : MonoBehaviour
    7. {
    8.     [ContextMenu("Extract")]
    9.     public void Extract()
    10.     {
    11.         Terrain terrain = GetComponent<Terrain>();
    12.         Transform[] transforms = terrain.GetComponentsInChildren<Transform>();
    13.  
    14.         //Skip the first, since its the Terrain Collider
    15.         for (int i = 1; i < transforms.Length; i++)
    16.         {
    17.             //Delete all previously created colliders first
    18.             DestroyImmediate(transforms[i].gameObject);
    19.         }
    20.  
    21.         for (int i = 0; i < terrain.terrainData.treePrototypes.Length; i++)
    22.         {
    23.             TreePrototype tree = terrain.terrainData.treePrototypes[i];
    24.  
    25.             //Get all instances matching the prefab index
    26.             TreeInstance[] instances = terrain.terrainData.treeInstances.Where(x => x.prototypeIndex == i).ToArray();
    27.  
    28.             for (int j = 0; j < instances.Length; j++)
    29.             {
    30.                 //Un-normalize positions so they're in world-space
    31.                 instances[j].position = Vector3.Scale(instances[j].position, terrain.terrainData.size);
    32.                 instances[j].position += terrain.GetPosition();
    33.  
    34.                 //Fetch the collider from the prefab object parent
    35.                 CapsuleCollider prefabCollider = tree.prefab.GetComponent<CapsuleCollider>();
    36.                 if(!prefabCollider) continue;
    37.  
    38.                 GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Capsule);
    39.                 obj.name = tree.prefab.name + j;
    40.  
    41.                 if (terrain.preserveTreePrototypeLayers) obj.layer = tree.prefab.layer;
    42.                 else obj.layer = terrain.gameObject.layer;
    43.  
    44.                 obj.transform.localScale = Vector3.one * prefabCollider.radius;
    45.                 obj.transform.position = instances[j].position;
    46.                 obj.transform.parent = terrain.transform;
    47.                 obj.isStatic = true;
    48.             }
    49.         }
    50.     }
    51. }
    52.  
    53.  
     
    Last edited: Oct 11, 2022
  4. MattCoachCarter

    MattCoachCarter

    Joined:
    Mar 14, 2020
    Posts:
    1
    Huge thank you to StaggartCreations and timmccune. It's insanity that tree nav mesh pathing/obstacle avoidance used to work in Unity but does no longer.

    I took timmccune's code and very slightly modified it so I wouldn't have to alter my trees' existing colliders. I added NavMeshObstacles to my trees, and then look for that component in the script instead.


    Code (CSharp):
    1.  
    2. using System.Linq;
    3. using UnityEngine;
    4. using UnityEngine.AI;
    5.  
    6. [RequireComponent(typeof(Terrain))]
    7. public class ExtractTreeCollidersFromTerrain : MonoBehaviour
    8. {
    9.     [ContextMenu("Extract")]
    10.     public void Extract()
    11.     {
    12.         Debug.Log("ExtractTreeCollidersFromTerrain::Extract");
    13.         Terrain terrain = GetComponent<Terrain>();
    14.         Transform[] transforms = terrain.GetComponentsInChildren<Transform>();
    15.         //Skip the first, since its the Terrain Collider
    16.         for (int i = 1; i < transforms.Length; i++)
    17.         {
    18.             //Delete all previously created colliders first
    19.             DestroyImmediate(transforms[i].gameObject);
    20.         }
    21.         Debug.Log("Tree prototypes count: "+ terrain.terrainData.treePrototypes.Length);
    22.         for (int i = 0; i < terrain.terrainData.treePrototypes.Length; i++)
    23.         {
    24.             TreePrototype tree = terrain.terrainData.treePrototypes[i];
    25.             //Get all instances matching the prefab index
    26.             TreeInstance[] instances = terrain.terrainData.treeInstances.Where(x => x.prototypeIndex == i).ToArray();
    27.             Debug.Log("Tree prototypes["+ i +"] instance count: "+ instances.Length);
    28.             for (int j = 0; j < instances.Length; j++)
    29.             {
    30.                 //Un-normalize positions so they're in world-space
    31.                 instances[j].position = Vector3.Scale(instances[j].position, terrain.terrainData.size);
    32.                 instances[j].position += terrain.GetPosition();
    33.                 NavMeshObstacle nav_mesh_obstacle = tree.prefab.GetComponent<NavMeshObstacle>();
    34.                 if(!nav_mesh_obstacle)
    35.                 {
    36.                     Debug.LogWarning("Tree with prototype["+ i +"] instance["+ j +"] did not have a NavMeshObstacle component, skipping!");
    37.                     continue;
    38.                 }
    39.  
    40.                 Vector3 primitive_scale = nav_mesh_obstacle.size;
    41.                 if(nav_mesh_obstacle.shape == NavMeshObstacleShape.Capsule)
    42.                 {
    43.                     primitive_scale = nav_mesh_obstacle.radius * Vector3.one;
    44.                 }
    45.                 GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Capsule);
    46.                 obj.name = tree.prefab.name + j;
    47.                 if (terrain.preserveTreePrototypeLayers) obj.layer = tree.prefab.layer;
    48.                 else obj.layer = terrain.gameObject.layer;
    49.                 obj.transform.localScale = primitive_scale;
    50.                 obj.transform.position = instances[j].position;
    51.                 obj.transform.parent = terrain.transform;
    52.                 obj.isStatic = true;
    53.             }
    54.         }
    55.     }
    56. }
    57.  

    For anyone who comes across this and doesn't understand how to use it:

    1. Add NavMeshObstacle components to all your tress in your terrain.
    2. Go to the tree tab in the terrain component and "Refesh" (this might not be needed but I did it.
    3. Add this script as a behavior component on your terrain
    4. Click the three little dots on the right side of the behavior component in the inspector window, and look for the "Extract" option. Click that option to run the code.
    5. You should see a ton of new game objects added to the scene, parented under your terrain object.
    6. Rebake your nav mesh
    7. Delete all the new objects created under your terrain, you don't need them anymore now that you've re-baked your nav mesh.

    If you ever need to rebake your nav mesh you'll have to run through those steps again, but for me it executed instantly, so it's no big deal.
     
    Ndogg27, Yaddak and dexmex001 like this.
  5. infinitypbr

    infinitypbr

    Joined:
    Nov 28, 2012
    Posts:
    3,149
    Thanks to you and all those before you :) NatureManufacture, such a wonderful group of folks, sent me here. I've refactored the code a bit, and also added a context menu to do the entire process at once.

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using System.Linq;
    3. using Unity.AI.Navigation;
    4. using UnityEngine;
    5. using UnityEngine.AI;
    6.  
    7. /// <summary>
    8. /// https://forum.unity.com/threads/navmeshsurface-and-terrain-trees.1295496/
    9. /// </summary>
    10. [RequireComponent(typeof(Terrain))]
    11. [RequireComponent(typeof(NavMeshSurface))]
    12. public class ExtractTreeCollidersFromTerrain : MonoBehaviour
    13. {
    14.     private readonly List<GameObject> _createdObjects = new List<GameObject>();
    15.    
    16.     private Terrain Terrain => GetTerrain();
    17.     private Terrain _terrain;
    18.    
    19.     private Terrain GetTerrain()
    20.     {
    21.         if (_terrain != null)
    22.             return _terrain;
    23.        
    24.         _terrain = GetComponent<Terrain>();
    25.         return _terrain;
    26.     }
    27.  
    28.     [ContextMenu("Extract, Bake, and Delete")]
    29.     public void ExtractBakeDelete()
    30.     {
    31.         var count = ExtractTreesFromTerrain();
    32.         GetComponent<NavMeshSurface>().BuildNavMesh();
    33.         DestroyCachedObjects();
    34.         Debug.Log($"<color=#99ff99>Successfully created {count} colliders and baked the NavMesh!</color>");
    35.     }
    36.  
    37.     [ContextMenu("Delete Collider Objects")]
    38.     private void DestroyCachedObjects()
    39.     {
    40.         foreach(var obj in _createdObjects)
    41.             DestroyImmediate(obj);
    42.        
    43.         _createdObjects.Clear();
    44.     }
    45.    
    46.     [ContextMenu("Extract Only")]
    47.     public int ExtractTreesFromTerrain()
    48.     {
    49.         for (var prototypeIndex = 0; prototypeIndex < Terrain.terrainData.treePrototypes.Length; prototypeIndex++)
    50.             ExtractInstancesFromTreePrototype(prototypeIndex);
    51.  
    52.         return _createdObjects.Count;
    53.     }
    54.    
    55.     private void ExtractInstancesFromTreePrototype(int prototypeIndex)
    56.     {
    57.         var tree = Terrain.terrainData.treePrototypes[prototypeIndex];
    58.         var instances = Terrain.terrainData.treeInstances.Where(x => x.prototypeIndex == prototypeIndex).ToArray();
    59.  
    60.         for (var instanceIndex = 0; instanceIndex < instances.Length; instanceIndex++)
    61.         {
    62.             UpdateInstancePosition(instances, instanceIndex);
    63.             CreateNavMeshObstacle(tree, prototypeIndex, instances, instanceIndex);
    64.         }
    65.     }
    66.    
    67.     private void CreateNavMeshObstacle(TreePrototype tree, int prototypeIndex, TreeInstance[] instances, int instanceIndex)
    68.     {
    69.         var navMeshObstacle = tree.prefab.GetComponent<NavMeshObstacle>();
    70.         if (!navMeshObstacle) return;
    71.  
    72.         var primitiveScale = CalculatePrimitiveScale(navMeshObstacle);
    73.  
    74.         var obj = CreateGameObjectForNavMeshObstacle(tree, instances, instanceIndex, primitiveScale);
    75.         _createdObjects.Add(obj);
    76.     }
    77.    
    78.     private GameObject CreateGameObjectForNavMeshObstacle(TreePrototype tree, TreeInstance[] instances, int instanceIndex, Vector3 primitiveScale)
    79.     {
    80.         var obj = GameObject.CreatePrimitive(PrimitiveType.Capsule);
    81.         obj.name = tree.prefab.name + instanceIndex;
    82.         obj.layer = Terrain.preserveTreePrototypeLayers ? tree.prefab.layer : Terrain.gameObject.layer;
    83.         obj.transform.localScale = primitiveScale;
    84.         obj.transform.position = instances[instanceIndex].position;
    85.         obj.transform.parent = Terrain.transform;
    86.         obj.isStatic = true;
    87.  
    88.         return obj;
    89.     }
    90.  
    91.     private Vector3 CalculatePrimitiveScale(NavMeshObstacle navMeshObstacle)
    92.     {
    93.         if (navMeshObstacle.shape == NavMeshObstacleShape.Capsule)
    94.             return navMeshObstacle.radius * Vector3.one;
    95.  
    96.         return navMeshObstacle.size;
    97.     }
    98.    
    99.     private void UpdateInstancePosition(TreeInstance[] instances, int instanceIndex)
    100.     {
    101.         instances[instanceIndex].position = Vector3.Scale(instances[instanceIndex].position, Terrain.terrainData.size);
    102.         instances[instanceIndex].position += Terrain.GetPosition();
    103.     }
    104. }
    The .unitypackage has this (will be in my LegendOfTheStones project directory)
     

    Attached Files:

    crdgre, neorocke and dexmex001 like this.
  6. unity_73DF5CB06F918670E195

    unity_73DF5CB06F918670E195

    Joined:
    Mar 20, 2021
    Posts:
    4
    This method works well for terrain trees, but I can't seem to make it work for terrain details such as rocks, tree stumps etc. Does anyone have any method that works for those?
     
    trombonaut likes this.
  7. tconkling

    tconkling

    Joined:
    Jul 13, 2012
    Posts:
    16
    These code snippets are hugely useful, thanks! They don't take into account the rotation and scale that Unity optionally applies to painted trees, and that should affect the capsule collider. To account for rotation and scale, you can modify your newly-created CapsuleCollider's transform like this:

    Code (CSharp):
    1. obj.transform.localScale = new Vector3(instances[j].widthScale, instances[j].heightScale, instances[j].widthScale);
    2. obj.transform.localRotation = Quaternion.Euler(0, Mathf.Rad2Deg * instances[j].rotation, 0);
    ("widthScale" is used to scale the tree on both the X and Z axes)
     
    crdgre likes this.
  8. zedz

    zedz

    Joined:
    Aug 31, 2013
    Posts:
    250
    What I do for rocks/trees etc is have a collider shape that is used only for the navmesh baking.
    I stick all these on the same Layer number.
    Then I have a simple function in the menu eg Tools -> SelectAllColliderLayers which goes through the whole scene and selects all these shapes, thus I can disable/enable them all in one click, very fast to do

    1. enable all collider shapes
    2. bake nav mesh
    3. disable all collider shapes