Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Navmesh Agent and Smooth Alignment with surface normals

Discussion in 'Editor & General Support' started by Legacy, May 14, 2012.

  1. Legacy

    Legacy

    Joined:
    Oct 11, 2011
    Posts:
    651
    Hello,

    My question is regarding the use of the unity Navmesh agent component and handling rotation of an object to align with the surface it is on(IE you have a Bug AI Agent pathing around on a terrain and it moves up a slope but does not align with the slope so its face ends up under ground and its tail and hind legs end up floating off the terrain). Now i know how to accomplish this but the issue is that the navmesh agent componenet takes explicit control over the objects rotation, if i disable its control it stops steering in the correct direction that its moving. Is there anyway around this or am i just stuck with it until unity adds some sort of functionality? Thanks in advance for your input.
     
  2. Legacy

    Legacy

    Joined:
    Oct 11, 2011
    Posts:
    651
    Anyone have any suggestions?
     
  3. Legacy

    Legacy

    Joined:
    Oct 11, 2011
    Posts:
    651
    Does anyone have any suggestions?
     
  4. Legacy

    Legacy

    Joined:
    Oct 11, 2011
    Posts:
    651
    Anyone at all? (offtopic sheesh this forum moves quick)
     
  5. Legacy

    Legacy

    Joined:
    Oct 11, 2011
    Posts:
    651
    Bump. Anyone?
     
  6. SalvadorLimones

    SalvadorLimones

    Joined:
    May 11, 2012
    Posts:
    7
    Just my 2 cents.

    Your agent mesh need not contain the navmesh agent.

    You can create a dummy invisible object and attach the navmesh agent to it. Then take your real actor/object/GO and make it a child of this navmeshagent (or constraint it to the dummy object that contains the navmeshagent in any manner you like using code.)

    Now, you can use localrotations to align the object with the normal of the topology as it is independent of the dummy.

    So lets say you have a soldier. Make a sphere and add navmeshagent component. Disable the meshrenderer of the sphere. Make this sphere the parent of the soldier. Now when the sphere moves, the soldier will move too. Control the localrotation of soldier as necessary using code. Use baseoffset if necessary to align the two.

    Havent tried it myself. let me know if it works.
     
    Last edited: May 22, 2012
  7. Legacy

    Legacy

    Joined:
    Oct 11, 2011
    Posts:
    651
    Haha that sounds like it will work perfectly, no idea why i didnt think of just parenting the model and controlling just the models rotation... Probably because my brain is fried from work and working on so many different game systems at once haha. Thanks a ton :)
     
  8. Legacy

    Legacy

    Joined:
    Oct 11, 2011
    Posts:
    651
    i did try this and i am able to align the model with the surface however i cannot get the character to look in the same direction as the steering target when the nav agent is walking, only the parent object obviously is doing that. Attempted to rotate to the surface normals and rotate towards its steering target but am having no luck, the model just faces one direction. Any ideas?
     
  9. Jakob_Unity

    Jakob_Unity

    Joined:
    Dec 25, 2011
    Posts:
    269
    So you basically would like to rotate twice - once for the up-vector and once for the heading (move direction). Is that right ?

    In that case I suggest you script it on your NavMeshAgent.. something along the lines of :

    Code (csharp):
    1. // TerrainMover.cs
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. public class TerrainMover : MonoBehaviour {
    6.   public Transform target;
    7.   public Terrain terrain;
    8.   private NavMeshAgent agent;
    9.   private Quaternion lookRotation;
    10.  
    11.   void Start () {
    12.     agent = GetComponent<NavMeshAgent>();  //< cache NavMeshAgent component
    13.     agent.updateRotation = false;          //< let us control the rotation explicitly
    14.     lookRotation = transform.rotation;     //< set original rotation
    15.   }
    16.  
    17.   Vector3 GetTerrainNormal () {
    18.     Vector3 terrainLocalPos = transform.position - terrain.transform.position;
    19.     Vector2 normalizedPos = new Vector2(terrainLocalPos.x / terrain.terrainData.size.x,
    20.                                         terrainLocalPos.z / terrain.terrainData.size.y);
    21.     return terrain.terrainData.GetInterpolatedNormal(normalizedPos.x, normalizedPos.y);
    22.   }
    23.  
    24.   void Update () {
    25.     agent.destination = target.position; //< update agent destination
    26.     Vector3 normal = GetTerrainNormal();
    27.     Vector3 direction = agent.steeringTarget - transform.position;
    28.     direction.y = 0.0f;
    29.     if(direction.magnitude > 0.1f  normal.magnitude > 0.1f) {
    30.       Quaternion qLook = Quaternion.LookRotation(direction, Vector3.up);
    31.       Quaternion qNorm = Quaternion.FromToRotation(Vector3.up, normal);
    32.       lookRotation = qNorm * qLook;
    33.     }
    34.     // soften the orientation
    35.     transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime/0.2f);
    36.   }
    37. }
    38.  
    39.  
    Make sure to place on NavMeshAgent and assign public properties (terrain, target) - there's no null-ref handling !..

    Cheers..
    /Jakob
     
    Last edited: Jun 19, 2012
  10. PatBGames

    PatBGames

    Joined:
    Mar 28, 2009
    Posts:
    120
    Thank you for this excellent piece of code.

    After tested it on my project, I was forced to change terrain.terrainData.size.y to z to get the normal correctly.

     
  11. -JohnMore-

    -JohnMore-

    Joined:
    Jun 16, 2013
    Posts:
    64
    I know it is a little bit old but I just happened to have the same problem and reached a different solution. I dont use terrains so I had to look for something different. Another problem was that I did not want to use the normal of the geometry under my objects, I wanted a smooth transition between the current orientation and the target orientation. So in the end I decided to use the LookAt function. This way, no matter what geometry I am moving on it just works.

    1-. In my Object script I put these variables:
    Code (csharp):
    1.         public bool OverrideRotation = false;
    2.         public float OrientationSpeed = 10f;
    3.         Vector3 lastPosition;
    4.         Vector3 position;
    5.         Vector3 direction;
    6.  
    2- Then, in the awake method:

    Code (csharp):
    1.                 if (OverrideRotation) {
    2.                         navAgent.updateRotation = false;
    3.                         lastPosition = myTransform.position;
    4.                 }
    5.  
    3- And last, if OverrideRotation is set to true, I start the coroutine that manages the orientation:

    Code (csharp):
    1.         IEnumerator updateRotation ()
    2.         {
    3.                 while (true) {
    4.                         Vector3 position = myTransform.position;
    5.                         direction = (position - lastPosition).normalized;
    6.  
    7.                         myTransform.LookAt (position + (Vector3.Lerp (myTransform.forward, direction, Time.deltaTime * OrientationSpeed)), Vector3.up);
    8.  
    9.                         lastPosition = position;
    10.  
    11.                         yield return Yields.Frame;
    12.                 }
    13.         }
    14.  
     
    UnityLighting and SpaceManDan like this.
  12. DiebeImDunkeln

    DiebeImDunkeln

    Joined:
    Nov 17, 2014
    Posts:
    8
    I use the script for my tank game, it works like charm, but there was something missing:


    Code (CSharp):
    1.  
    2. if(direction.magnitude > 0.1f  normal.magnitude > 0.1f)
    3. {
    4.       Quaternion qLook = Quaternion.LookRotation(direction, Vector3.up);
    5.       Quaternion qNorm = Quaternion.FromToRotation(Vector3.up, normal);
    6.       lookRotation = qNorm * qLook;
    7.  }
    8.  
    In the first line is an "OR" missing: if(direction.magnitude > 0.1f || normal.magnitude > 0.1f)
    And for me it works better if I set it to 3.0f instead of 0.1f. Otherwise my tank reaches the target point and rotates randomly.
     
    andrew-lukasik likes this.
  13. andrew-lukasik

    andrew-lukasik

    Joined:
    Jan 31, 2013
    Posts:
    249
    Omg I wrestled with this problem for days. Thank you @DiebeImDunkeln !! :D

    And thanks to that I found even better solution (for my specific case at least)
    Code (csharp):
    1. ROTATION = Quaternion.LookRotation(
    2.     Vector3.ProjectOnPlane( DIRECTION , SURFACENORMAL ) ,
    3.     SURFACENORMAL
    4. );
     
    Last edited: Jan 22, 2016
  14. aikijeet

    aikijeet

    Joined:
    Jun 19, 2013
    Posts:
    3
    have you tried colliding with another navmesh agent in the scene? (ex. another moving object like a car)
    my setup involves a control override on my main vehicle that moves it left and right (shift lanes) and when it collides on the walls or another collider, the child object (the car mesh) is pushed away from the parent (navMesh agent) and misaligns the two, therefore causing the car to rotate itself abnormally (facing other directions while moving forward) and sometimes flips over

    I am using rigidbody and a box collider on my car gameobject with a script to control the NavMesh agent to move left and right

    I tried using Transform.LookAt but failed miserably (caused more abnormal rotations during collisions)
    I have also tried matching both their localrotations during FixedUpdate but it doesn't seem to work...

    Any suggestions how I can make this work? --- collision works normally if I put the navmeshagent component directly on my car gameObject but as the topic indicates, it does not inherit the gravity component that makes it land on the surface normally and not rotate during uphills or downhills

    thanks
     
  15. andrew-lukasik

    andrew-lukasik

    Joined:
    Jan 31, 2013
    Posts:
    249
    @aikijeet I suggest you ditch that Rigidbody+Collider and animate car mesh (as child of NavMeshAgent) from your code alone.
    Using Physics.Raycast get ground position and normal vectors. After that you can Lerp positions between present and ground hit.point to simulate gravity. And Lerp rotation between present and that of ground normal to achieve good surface aligment
     
  16. muzboz

    muzboz

    Joined:
    Aug 8, 2012
    Posts:
    108
    I used @Jakob_Unity 's code, but changed things around a bit like this...

    IMPORTANT NOTE: The structure of the character hierarchy is important!
    I have an empty game object, and that has the NavMeshAgent on it, and the AI script.
    Then I have an empty game object under that, which I call CharacterModel.
    For me, this is just a transform. It has no other components on it.
    Then there is another child under that, which is the actual skinned mesh character.

    It looks like this:
    • Deer (transform, animator, audio source, nav mesh agent, AI script
      • CharacterModel (transform)
        • Deer_Body (transform, skinned mesh renderer, material)
    The rotation is done on the CharacterModel game object, so as to not interfere with the parent object's NavMesh steering, etc.


    The relevant bits of code look like this:

    Code (CSharp):
    1. public Quaternion previousRotation;
    2. public Quaternion desiredRotation;
    3. public Quaternion currentRotation;
    4. private LayerMask terrainLayermask;
    5. public GameObject characterModel; // assign CharacterModel game object to this in Inspector  
    ...

    Code (CSharp):
    1. void Start()
    2.     {
    3.         // requires Terrain layer to be named "Terrain" in your scene
    4.         terrainLayermask = LayerMask.GetMask("Terrain");
    5.     }
    ...

    Code (CSharp):
    1. void AlignToTerrain()
    2.     {
    3.         RaycastHit hit;
    4.         if (Physics.Raycast(new Vector3(transform.position.x, transform.position.y + 2,
    5.                             transform.position.z), -Vector3.up, out hit, 3, terrainLayermask))
    6.         {
    7.             Quaternion qLook = Quaternion.LookRotation(transform.forward, Vector3.up);
    8.             Quaternion qNorm = Quaternion.FromToRotation(Vector3.up, hit.normal);
    9.             desiredRotation = qNorm * qLook;
    10.             // smooths angle change each frame
    11.             currentRotation = Quaternion.Slerp(previousRotation, desiredRotation, 0.03f);
    12.             characterModel.transform.rotation = currentRotation;
    13.             previousRotation = currentRotation;
    14.         }
    15.  
    16.     }

    And I call AlignToTerrain() in the Update() loop.
     
    Last edited: Nov 11, 2019
  17. ede0m

    ede0m

    Joined:
    Dec 12, 2020
    Posts:
    7

    Hey Jakob. I'm wondering if you can explain the reason that lookRotation is a composition of two quaternions? I've used similar code in my project and notice that Quaternion.FromToRotation does provide the rotation I am looking for in order to align with a ramp, however, once I begin interpolating TO that rotation (qNorm in your code), my rotation never fully completes.

    My intuition is because qNorm rotation value will decrease every update call as you converge on alignment, and the interpolation param t is small enough that it keeps the interpolation on the lower end. Is this reasoning correct?

    If so, how does composing qLook and qNorm provide the desired rotation? It would essentially be tacking on the new rotation every update call. In my project the code looks like

    Code (csharp):
    1.  
    2.         if (!grounded) {
    3.             if (Physics.Raycast(transform.position, -transform.up, out RaycastHit hit, alignGroundProbeDistance)) {
    4.                 var toRotation = Quaternion.FromToRotation(transform.up, hit.normal);
    5.                 var lookRotation = toRotation * transform.rotation;
    6.                 transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * 2);
    7.             }
    8.         }
    9.  
    10.  
    Thanks for any insight.
     
  18. m_AlphaSmail

    m_AlphaSmail

    Joined:
    Sep 16, 2019
    Posts:
    2
    Sheesh has been used in 2012, wow what a thing I have discovered.
     
  19. devinrojas

    devinrojas

    Joined:
    Oct 12, 2022
    Posts:
    1
    This just saved me life. I have been looking for this solution for along time, thank you!