Does the component navigation work with terrain trees yet?

It seemed pretty decent, but it has (had?) two serious flaws:

  • It won’t work if you have multiple scene files loaded. It throws errors.
  • It doesn’t work with trees on the terrain. It just ignores them.

I can’t comment on the multiple scene issue, but have you tried giving your tree prefabs NavMeshModifierVolumes?

It does not work with trees out of the box like the built in navmesh system. Please comment on the issue on github so we can finally have this fixed https://github.com/Unity-Technologies/NavMeshComponents/issues/30

If you know any other solution please let me know, I haven’t tried giving the trees NavMeshModifierVolumes and I cannot test it right now, if someone can do that would be awesome.

Have you tried giving your tree prefabs NavMeshModifierVolumes ? It is not working for me :frowning:

How do you bake/update your navmeshes? With NavMeshSurface or LocalNavMeshBuilder?

I have a navmesh surface component in the scene and I click the Bake button from the navmesh surface script component.

If I use Navigation > Bake, I am not really happy with the results, I see some parts of the terrain not baked properly, where the navmesh surface bake does it just right.

Well, that just doesn’t work. The “obsolete” bake in the Unity UI does not work with NavMeshModifiers and NavMeshModifierVolumes. You need to use the bake function of the NavMeshSurface from the new Navgiation components.

Can you post a screenshot, of the terrain so we can see why it does not bake properly?

It has gaps that are not present when I bake from the NM Surface component, so I want to stick with that. The problem is that it does not detect unity’s terrain trees :frowning:

AFAIK, there is no solution for this if you stick using the old navmesh bake function.

Well I am not using the old bake function and by old I am refering to the built in thats in the Navigation window. I am using the one from the navmesh surface game object.

Hi,
I ran into the same issue with terrain tree and NavMeshSurface and wasn’t able to find a solution on google !
So I tried to patch NavMeshSurface script myself and I’m quite happy with the result !

The code is still missing a lot of things like using the real size of the tree (you can use treePrototypes to retrieve the tree prefab), colliders and details meshes, but it is suited to my needs !
It is just an example on how to implement tree baking in NavMeshSurface.

Replace NavMeshSurface.CollectSource() with this method.
(Added code is between /***** Terrain trees / and / End ofTerrain trees *****/)

There is still an issue if your terrain is not set to position 0, 0, 0
The fakeGameObject.transform.position should take into account terrain transform (position, rotation and scale)

Hope it can help some people !
(and sorry for my bad english !)

        List<NavMeshBuildSource> CollectSources()
        {
            var sources = new List<NavMeshBuildSource>();
            var markups = new List<NavMeshBuildMarkup>();

            List<NavMeshModifier> modifiers;
            if (m_CollectObjects == CollectObjects.Children)
            {
                modifiers = new List<NavMeshModifier>(GetComponentsInChildren<NavMeshModifier>());
                modifiers.RemoveAll(x => !x.isActiveAndEnabled);
            }
            else
            {
                modifiers = NavMeshModifier.activeModifiers;
            }

            foreach (var m in modifiers)
            {
                if ((m_LayerMask & (1 << m.gameObject.layer)) == 0)
                    continue;
                if (!m.AffectsAgentType(m_AgentTypeID))
                    continue;
                var markup = new NavMeshBuildMarkup();
                markup.root = m.transform;
                markup.overrideArea = m.overrideArea;
                markup.area = m.area;
                markup.ignoreFromBuild = m.ignoreFromBuild;
                markups.Add(markup);
            }
         
            if (m_CollectObjects == CollectObjects.All)
            {
                NavMeshBuilder.CollectSources(null, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }
            else if (m_CollectObjects == CollectObjects.Children)
            {
                NavMeshBuilder.CollectSources(transform, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }
            else if (m_CollectObjects == CollectObjects.Volume)
            {
                Matrix4x4 localToWorld = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
                var worldBounds = GetWorldBounds(localToWorld, new Bounds(m_Center, m_Size));
                NavMeshBuilder.CollectSources(worldBounds, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }

            /***** Terrain trees *****/

            var fakeGameObject = new GameObject();
            // Required to have a Transform instance to call localToWorldMatrix (I'm fairly bad at mathematics so maybe there is an other solution !)

            var size = Terrain.activeTerrain.terrainData.size;
            // Do not use Terrain.activeTerrain but maybe add a properties or detect terrains inside the bounding box of NavMeshSurface
            // Then simply loop through all selected terrains
            foreach (var tree in Terrain.activeTerrain.terrainData.treeInstances)
            {
                var prototype = Terrain.activeTerrain.terrainData.treePrototypes[tree.prototypeIndex];
                // Use prototype.prefab to reach the prefab of the tree
                // not used in this example

                fakeGameObject.transform.position = new Vector3(tree.position.x * size.x, tree.position.y * size.y, tree.position.z * size.z) ;
                fakeGameObject.transform.rotation = Quaternion.AngleAxis(tree.rotation, Vector3.up);
                fakeGameObject.transform.localScale = new Vector3(1, 1, 1);

                var src = new NavMeshBuildSource();
                src.transform = fakeGameObject.transform.localToWorldMatrix;
                src.shape = NavMeshBuildSourceShape.Box; // update this to your convenience
                src.size = new Vector3(1f, 1f, 1f); // update this according to tree heightScale / widthScale and the prefab size.
                sources.Add(src);
            }
            DestroyImmediate(fakeGameObject);

            /***** End ofTerrain trees *****/

            if (m_IgnoreNavMeshAgent)
                sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshAgent>() != null));

            if (m_IgnoreNavMeshObstacle)
                sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshObstacle>() != null));

            AppendModifierVolumes(ref sources);

            return sources;
        }
5 Likes

Thanks for the workaround, I haven’t tested it, because I found another solution - exporting the tree colliders in the scene and rebaking the navmesh. Hopefully your solution will help to someone!

Thanks ! Its work fine.

I add terran position:

public Terrain mTerrain;

Vector3 pos = mTerrain.transform.position;

fakeGameObject.transform.position = new Vector3(tree.position.x * size.x + pos.x,
tree.position.y * size.y + pos.y,
tree.position.z * size.z + pos.z);

1 Like

This is still not working, tried to use NavMesh components but they ignore terrain trees for some reason if I bake with NavMeshSurface. Can somebody provide a solution? Tried above and still the same, if I place trees manually it works but from terrain trees, it does not work at all. Please help somebody

I dont have other solution than buying A* project and using it, thats what I ended up doing.

We are making MMO (for a while now) and these navigation problems are so annoying. I Was hoping that they will just fix that baking problem with trees and we could just use that. You can see on this example image (some terrain where I tried to bake things properly) I do get weird path around trees if default Unity Navigation Bake is used, looks really weird in the game when players are running around. NavMeshSurface has option to bake colliders only so when I try to use that i do get better results but only if trees are placed by hand, not terrain trees sadly… Would be great to not go and use A* at this point since we based on NavMeshAgent our combat, network, navigation…But if we have to switch as @skinwalker_1 suggested then we will probably.

2 Likes

Hi! I fixed the thing where if the terrain is not on zero it doesn’t work.
Here’s the whole thing independent of terrain position:

 List<NavMeshBuildSource> CollectSources()
        {
            var sources = new List<NavMeshBuildSource>();
            var markups = new List<NavMeshBuildMarkup>();

            List<NavMeshModifier> modifiers;
            if (m_CollectObjects == CollectObjects.Children)
            {
                modifiers = new List<NavMeshModifier>(GetComponentsInChildren<NavMeshModifier>());
                modifiers.RemoveAll(x => !x.isActiveAndEnabled);
            }
            else
            {
                modifiers = NavMeshModifier.activeModifiers;
            }

            foreach (var m in modifiers)
            {
                if ((m_LayerMask & (1 << m.gameObject.layer)) == 0)
                    continue;
                if (!m.AffectsAgentType(m_AgentTypeID))
                    continue;
                var markup = new NavMeshBuildMarkup();
                markup.root = m.transform;
                markup.overrideArea = m.overrideArea;
                markup.area = m.area;
                markup.ignoreFromBuild = m.ignoreFromBuild;
                markups.Add(markup);
            }

            if (m_CollectObjects == CollectObjects.All)
            {
                NavMeshBuilder.CollectSources(null, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }
            else if (m_CollectObjects == CollectObjects.Children)
            {
                NavMeshBuilder.CollectSources(transform, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }
            else if (m_CollectObjects == CollectObjects.Volume)
            {
                Matrix4x4 localToWorld = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
                var worldBounds = GetWorldBounds(localToWorld, new Bounds(m_Center, m_Size));
                NavMeshBuilder.CollectSources(worldBounds, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }

            /***** Terrain trees *****/

            var fakeGameObject = new GameObject();
            // Required to have a Transform instance to call localToWorldMatrix (I'm fairly bad at mathematics so maybe there is an other solution !)

            var size = Terrain.activeTerrain.terrainData.size;
            // Do not use Terrain.activeTerrain but maybe add a properties or detect terrains inside the bounding box of NavMeshSurface
            // Then simply loop through all selected terrains
            foreach (var tree in Terrain.activeTerrain.terrainData.treeInstances)
            {
                var prototype = Terrain.activeTerrain.terrainData.treePrototypes[tree.prototypeIndex];
                // Use prototype.prefab to reach the prefab of the tree
                // not used in this example

                fakeGameObject.transform.position = new Vector3(tree.position.x * size.x, tree.position.y * size.y, tree.position.z * size.z);
                fakeGameObject.transform.position += Terrain.activeTerrain.GetPosition();
                fakeGameObject.transform.rotation = Quaternion.AngleAxis(tree.rotation, Vector3.up);
                fakeGameObject.transform.localScale = new Vector3(1, 1, 1);

                var src = new NavMeshBuildSource();
                src.transform = fakeGameObject.transform.localToWorldMatrix;
                src.shape = NavMeshBuildSourceShape.Box; // update this to your convenience
                src.size = new Vector3(1f, 1f, 1f); // update this according to tree heightScale / widthScale and the prefab size.
                sources.Add(src);
            }
            DestroyImmediate(fakeGameObject);

            /***** End ofTerrain trees *****/

            if (m_IgnoreNavMeshAgent)
                sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshAgent>() != null));

            if (m_IgnoreNavMeshObstacle)
                sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshObstacle>() != null));

            AppendModifierVolumes(ref sources);

            return sources;
        }
2 Likes

For us non engineers. I can’t find “NavMeshSurface.CollectSource()” has something changed? Can someone provide the entire script here?

(Really annoying that the nav mesh doesn’t bake Trees)

Works great if change fakeGameObject.transform.localScale from Vector3(1, 1, 1) to Vector3(2, 10, 2);

        List<NavMeshBuildSource> CollectSources()
        {
            var sources = new List<NavMeshBuildSource>();
            var markups = new List<NavMeshBuildMarkup>();

            List<NavMeshModifier> modifiers;
            if (m_CollectObjects == CollectObjects.Children)
            {
                modifiers = new List<NavMeshModifier>(GetComponentsInChildren<NavMeshModifier>());
                modifiers.RemoveAll(x => !x.isActiveAndEnabled);
            }
            else
            {
                modifiers = NavMeshModifier.activeModifiers;
            }

            foreach (var m in modifiers)
            {
                if ((m_LayerMask & (1 << m.gameObject.layer)) == 0)
                    continue;
                if (!m.AffectsAgentType(m_AgentTypeID))
                    continue;
                var markup = new NavMeshBuildMarkup();
                markup.root = m.transform;
                markup.overrideArea = m.overrideArea;
                markup.area = m.area;
                markup.ignoreFromBuild = m.ignoreFromBuild;
                markups.Add(markup);
            }

            if (m_CollectObjects == CollectObjects.All)
            {
                NavMeshBuilder.CollectSources(null, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }
            else if (m_CollectObjects == CollectObjects.Children)
            {
                NavMeshBuilder.CollectSources(transform, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }
            else if (m_CollectObjects == CollectObjects.Volume)
            {
                Matrix4x4 localToWorld = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one);
                var worldBounds = GetWorldBounds(localToWorld, new Bounds(m_Center, m_Size));
                NavMeshBuilder.CollectSources(worldBounds, m_LayerMask, m_UseGeometry, m_DefaultArea, markups, sources);
            }

            /***** Terrain trees *****/

            var fakeGameObject = new GameObject();
            // Required to have a Transform instance to call localToWorldMatrix (I'm fairly bad at mathematics so maybe there is an other solution !)

            var size = Terrain.activeTerrain.terrainData.size;
            // Do not use Terrain.activeTerrain but maybe add a properties or detect terrains inside the bounding box of NavMeshSurface
            // Then simply loop through all selected terrains
            foreach (var tree in Terrain.activeTerrain.terrainData.treeInstances)
            {
                var prototype = Terrain.activeTerrain.terrainData.treePrototypes[tree.prototypeIndex];
                // Use prototype.prefab to reach the prefab of the tree
                // not used in this example

                fakeGameObject.transform.position = new Vector3(tree.position.x * size.x, tree.position.y * size.y, tree.position.z * size.z);
                fakeGameObject.transform.position += Terrain.activeTerrain.GetPosition();
                fakeGameObject.transform.rotation = Quaternion.AngleAxis(tree.rotation, Vector3.up);
                fakeGameObject.transform.localScale = new Vector3(2, 10, 2);

                var src = new NavMeshBuildSource();
                src.transform = fakeGameObject.transform.localToWorldMatrix;
                src.shape = NavMeshBuildSourceShape.Box; // update this to your convenience
                src.size = new Vector3(1f, 1f, 1f); // update this according to tree heightScale / widthScale and the prefab size.
                sources.Add(src);
            }
            DestroyImmediate(fakeGameObject);

            /***** End ofTerrain trees *****/

            if (m_IgnoreNavMeshAgent)
                sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshAgent>() != null));

            if (m_IgnoreNavMeshObstacle)
                sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshObstacle>() != null));

            AppendModifierVolumes(ref sources);

            return sources;
        }
1 Like

Thank you so much, that was a lot to get me started on the solution I needed. I made some adjustments, including optimizations with getting arrays from the Unity API multiple times, and avoiding the need to create GameObjects. I used Matrix4x4.TRS(…) instead to calculate the localToWorld matrix, even though I don’t know how to calculate that mathematically, myself yet :wink: haha.

My version (below) adds support for trees with NavMeshObstacles on their prefab’s root GameObject. Specifically, if it DOES find a NavMeshObstacle on the prefab’s root GameObject, it will add that as a source to the list, and use the center and sizing of the obstacle.

I left my commented-out Debug.Log(…) calls in case you wanted to easily uncomment them and see what’s going on.

I can only hope that Unity includes something this vital for the NavMeshComponents. If I get time, I may request it to them, and say “hey, it works already! Here, … please!!!” haha. Goodluck, and hope this helps :slight_smile:

// --- --- --- Terrain trees --- --- ---

Terrain terrain = Terrain.activeTerrain;
TerrainData terrainData = terrain.terrainData;
Vector3 size = Terrain.activeTerrain.terrainData.size;
Vector3 terrainPos = terrain.GetPosition();
//Debug.Log("terrainPos = " + terrainPos);
int treesArea = NavMesh.GetAreaFromName("Not Walkable");
if (treesArea < 0) {
    Debug.LogError("Unrecognized area name! The default area will be used instead.");
    treesArea = 0;
}

TreePrototype[] treePrototypes = terrainData.treePrototypes;
TreeInstance[] treeInstances = terrainData.treeInstances;
for (int i = 0; i < treeInstances.Length; i++) {
    //treeInstances[i] is the current ACTUAL tree we're going over.
    //the tree prototype is the "template" used by this tree.
    TreePrototype prototype = treePrototypes[treeInstances[i].prototypeIndex];
    GameObject prefab = prototype.prefab;
    NavMeshObstacle obstacle = prefab.GetComponent<NavMeshObstacle>();
    if (obstacle == null)
        continue;

    //Debug.Log("treeInstances[" + i + "] info:\n" + treeInstances[i].position + " " + treeInstances[i].rotation + " " + treeInstances[i].widthScale + " " + treeInstances[i].heightScale);
    Vector3 worldTreePos = terrainPos + Vector3.Scale(treeInstances[i].position, size)
        + obstacle.center;
    Quaternion worldTreeRot = Quaternion.Euler(0, treeInstances[i].rotation * Mathf.Rad2Deg, 0);
    Vector3 worldTreeScale = new Vector3(treeInstances[i].widthScale, treeInstances[i].heightScale, treeInstances[i].widthScale);
    //Debug.Log("CREATED MATRIX FOR TRS:\nworldTreePos = " + worldTreePos + "\nworldTreeRot = " + worldTreeRot + "\nworldTreeScale = " + worldTreeScale);
   
    NavMeshBuildSource src = new NavMeshBuildSource();
    src.transform = Matrix4x4.TRS(worldTreePos, worldTreeRot, worldTreeScale);

    switch (obstacle.shape) {
        case NavMeshObstacleShape.Capsule:
            src.shape = NavMeshBuildSourceShape.Capsule;

            //Unity 2019.2.0f1: BUG!! navMeshObstacle.height returns exactly HALF of the actual height of the obstacle.
            //Use the size property instead.
            src.size = obstacle.size;
            break;
        case NavMeshObstacleShape.Box:
            src.shape = NavMeshBuildSourceShape.Box;
            src.size = obstacle.size;
            break;
        default:
            Debug.LogError("Unsupported type of " + typeof(NavMeshObstacleShape).Name
                + " for the building of the " + typeof(NavMeshSurface).Name + "! (" + obstacle.shape + ")");
            break;
    }
    src.size = Vector3.Scale(src.size, prefab.transform.localScale);
    //Debug.Log("src.size = " + src.size);
    src.area = treesArea;
    sources.Add(src);
}

// --- --- --- End of Terrain trees --- --- ---

[EDIT]: This code should go in the NavMeshSurface.CollectSources() method. Exactly where may depend on the version of the NavMeshComponents repo that you cloned, I used the 2019.1 branch. I put it right before the lines that use these “ignore” options:

if (m_IgnoreNavMeshAgent)
    sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshAgent>() != null));

if (m_IgnoreNavMeshObstacle)
    sources.RemoveAll((x) => (x.component != null && x.component.gameObject.GetComponent<NavMeshObstacle>() != null));

This should be near the end of the method where it returns the sources.

4 Likes