Navmesh Agent and Smooth Alignment with surface normals

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.

Anyone have any suggestions?

Does anyone have any suggestions?

Anyone at all? (offtopic sheesh this forum moves quick)

Bump. Anyone?

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.

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 :slight_smile:

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?

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 :

// TerrainMover.cs
using UnityEngine;
using System.Collections;

public class TerrainMover : MonoBehaviour {
  public Transform target;
  public Terrain terrain;
  private NavMeshAgent agent;
  private Quaternion lookRotation;
  
  void Start () {
    agent = GetComponent<NavMeshAgent>();  //< cache NavMeshAgent component
    agent.updateRotation = false;          //< let us control the rotation explicitly
    lookRotation = transform.rotation;     //< set original rotation
  }
  
  Vector3 GetTerrainNormal () {
    Vector3 terrainLocalPos = transform.position - terrain.transform.position;
    Vector2 normalizedPos = new Vector2(terrainLocalPos.x / terrain.terrainData.size.x,
                                        terrainLocalPos.z / terrain.terrainData.size.y);
    return terrain.terrainData.GetInterpolatedNormal(normalizedPos.x, normalizedPos.y);
  }

  void Update () {
    agent.destination = target.position; //< update agent destination
    Vector3 normal = GetTerrainNormal();
    Vector3 direction = agent.steeringTarget - transform.position;
    direction.y = 0.0f;
    if(direction.magnitude > 0.1f  normal.magnitude > 0.1f) {
      Quaternion qLook = Quaternion.LookRotation(direction, Vector3.up);
      Quaternion qNorm = Quaternion.FromToRotation(Vector3.up, normal);
      lookRotation = qNorm * qLook;
    }
    // soften the orientation
    transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime/0.2f);
  }
}

Make sure to place on NavMeshAgent and assign public properties (terrain, target) - there’s no null-ref handling !..

Cheers…
/Jakob

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.

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:

		public bool OverrideRotation = false;
		public float OrientationSpeed = 10f;
		Vector3 lastPosition;
		Vector3 position;
		Vector3 direction;

2- Then, in the awake method:

				if (OverrideRotation) {
						navAgent.updateRotation = false;
						lastPosition = myTransform.position;
				}

3- And last, if OverrideRotation is set to true, I start the coroutine that manages the orientation:

		IEnumerator updateRotation ()
		{
				while (true) {
						Vector3 position = myTransform.position;
						direction = (position - lastPosition).normalized;

						myTransform.LookAt (position + (Vector3.Lerp (myTransform.forward, direction, Time.deltaTime * OrientationSpeed)), Vector3.up);

						lastPosition = position;

						yield return Yields.Frame;
				}
		}
2 Likes

I use the script for my tank game, it works like charm, but there was something missing:

if(direction.magnitude > 0.1f  normal.magnitude > 0.1f)
{
      Quaternion qLook = Quaternion.LookRotation(direction, Vector3.up);
      Quaternion qNorm = Quaternion.FromToRotation(Vector3.up, normal);
      lookRotation = qNorm * qLook;
 }

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.

1 Like

Omg I wrestled with this problem for days. Thank you @DiebeImDunkeln !! :smile:

And thanks to that I found even better solution (for my specific case at least)

ROTATION = Quaternion.LookRotation(
    Vector3.ProjectOnPlane( DIRECTION , SURFACENORMAL ) ,
    SURFACENORMAL
);

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

@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

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:

public Quaternion previousRotation;
public Quaternion desiredRotation;
public Quaternion currentRotation;
private LayerMask terrainLayermask;
public GameObject characterModel; // assign CharacterModel game object to this in Inspector

…

void Start()
    {
        // requires Terrain layer to be named "Terrain" in your scene
        terrainLayermask = LayerMask.GetMask("Terrain"); 
    }

…

void AlignToTerrain()
    {
        RaycastHit hit;
        if (Physics.Raycast(new Vector3(transform.position.x, transform.position.y + 2,
                            transform.position.z), -Vector3.up, out hit, 3, terrainLayermask))
        {
            Quaternion qLook = Quaternion.LookRotation(transform.forward, Vector3.up);
            Quaternion qNorm = Quaternion.FromToRotation(Vector3.up, hit.normal);
            desiredRotation = qNorm * qLook;
            // smooths angle change each frame
            currentRotation = Quaternion.Slerp(previousRotation, desiredRotation, 0.03f);
            characterModel.transform.rotation = currentRotation;
            previousRotation = currentRotation;
        }

    }

And I call AlignToTerrain() in the Update() loop.

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

        if (!grounded) {
            if (Physics.Raycast(transform.position, -transform.up, out RaycastHit hit, alignGroundProbeDistance)) {
                var toRotation = Quaternion.FromToRotation(transform.up, hit.normal);
                var lookRotation = toRotation * transform.rotation; 
                transform.rotation = Quaternion.Slerp(transform.rotation, lookRotation, Time.deltaTime * 2);
            }
        }

Thanks for any insight.

Sheesh has been used in 2012, wow what a thing I have discovered.

This just saved me life. I have been looking for this solution for along time, thank you!