Rotation to Surface Normal Issues

I’m attempting to make a character that can run through a loop. I’m using the following code to rotate the character to match the surface normal:

RaycastHit hit;
        Vector3 pos = new Vector3 (transform.position.x, transform.position.y + 0.1f, transform.position.z);
        if (Physics.Raycast (pos, -transform.up, out hit, 0.3f)) {


            transform.rotation = Quaternion.Slerp (transform.rotation, Quaternion.FromToRotation (transform.up, hit.normal) * transform.rotation, Time.deltaTime * 15f);
}

This works fine up until the character rotates more than 90 degrees. This graph shows kind of what I’m explaining:

The capsule represents the character and the red line represents the raycast.


Anyone know why this is?

Is it actually colliding the whole time? You might change the ray position to vectorPos = transform.position + transform.up *0.1.

I’m assuming your trying to offset the ray position a little so the collision registers but in your code your accidentally offsetting in the positive up direction, when you want to be offsetting in the up direction of the capsule.

2 Likes

Thank you! I’m having another issue regarding rotation. I don’t think I should move it to a new thread since it ties into this problem.
I’m using this code to rotate the character based on the Vertical and Horizontal Input Axis and the direction the camera is facing:

        float myAngle = Mathf.Atan2 (Input.GetAxis ("Horizontal"), Input.GetAxis ("Vertical")) * Mathf.Rad2Deg;

        float bodyRotation = myAngle + forward.transform.eulerAngles.y;

        Vector2 inputPower = new Vector2 (Input.GetAxis ("Horizontal"), Input.GetAxis ("Vertical"));

        anim.SetFloat ("MoveSpeed", rb.velocity.magnitude);

        float rotationValue = Input.GetAxis ("Horizontal") * 90f;
        Quaternion originalRot = transform.rotation;

        if (inputPower.magnitude > 0.1) {
            transform.rotation = Quaternion.Slerp (transform.rotation, Quaternion.Euler (transform.eulerAngles.x, bodyRotation, transform.eulerAngles.z), Time.deltaTime * 10f);
        }

The problem is that if I’m on an uneven surface, when I go to turn it seems to rotate the player as if they are standing vertically rather than diagonally on the surface.

The problem i think is in your use of Quaternion.Euler(). It rotates around the world xyz, not the localy xyz, which is what you want. If you want to rotate around the world xz, but the local up axis, try storing it as 2 different quaternions and multiplying them, such as Quaternion.Euler(x, 0, y), and the second Quaternion.AngleAxis(bodyRotation, capsule.up).

        Quaternion newRot = Quaternion.AngleAxis (bodyRotation, transform.up);

        Quaternion oRot = new Quaternion (transform.rotation.x, 0, transform.rotation.z, transform.rotation.w);

        if (inputPower.magnitude > 0.1) {
            transform.rotation = newRot * oRot;
        }

Do you mean like this? I’m still having the same issue. I tried Quaternion.Euler for oRot as well with the same result.

try oRot * newRot and see if that fixes it. the order of the rotations is important. But what are you trying to achieve exactly? I’m slightly confused.

You have an object, i’m assuming a 3rd person perspective, you said you wanted to rotate it based off of the camera, but i do not see anything representing the camera in your earlier code.

and you appear to want to rotate the object when it is aligned to a surface that it is standing on, is that about right?

here is some pseudo code for what i think you are trying to do:

// In your previous code example i noticed your generating an angle from the x and y user inputs but i couldn't see a
// reason for this? Am i wrong?
float angleFromInput =  Input.Axis("Horizontal") * Time.deltaTime * desiredRotationSpeed;

// From here what is your input actually used for? is it to rotate around the local up vector?

transform.rotation *= Quaterion.AngleAxis(angleFromInput, transform.up);

// I'm also not sure at which point your object needs to be aligned to the surface normal, but you might need that
// here next
transform.rotation *= Quaternion.FromTo(transform.up, surfaceNormal);

the GameObject “forward” is the camera object.

float myAngle = Mathf.Atan2 (Input.GetAxis ("Horizontal"), Input.GetAxis ("Vertical")) * Mathf.Rad2Deg;
float bodyRotation = myAngle + forward.transform.eulerAngles.y;

It didn’t seem to change the result when I reversed the order in which the quaternions were multiplied.

The goal is to have the player’s up direction match the surface normal and still maintain the ability to rotate the player and turn using the input axis relative to the character’s current rotation, Similar to the Sonic the Hedgehog games. When you hold down the “D” key, he runs in the camera’s right direction, if you hold down the “W” key, he runs in the camera’s forward direction while still maintaining the ability to run up walls and loops. The code you posted doesn’t seem to resolve this issue.


This image should show what I’m trying to do.

transform.rotation *= Quaterion.AngleAxis(angleFromInput, transform.up);

This works except turning left causes the character to rapidly spin around counter-clockwise instead of facing the direction of the joystick axis relative to camera rotation.

Alright so this is pretty quick and dirty. Maybe when i’ll play with it more when i’m done pulling my hair out over my own code. But this “mostly” works. You will have to play with it. There will be some issues, mostly in defining what is forward as your characters orientation changes. Rotating the cameras up to match the players up might be the best bet.

using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class TimsSimpleMotor : MonoBehaviour
{
    [Tooltip("The speed of our motor.")]
    public float movementSpeed = 5f;
    [Tooltip("The layer mask for our ground detector ray.")]
    public LayerMask layerMask;

    CharacterController charController;

    // Use this for initialization
    void Start ()
    {
        charController = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update ()
    {
        float x = Input.GetAxis("Horizontal") * Time.deltaTime;
        float y = Input.GetAxis("Vertical") * Time.deltaTime;

        Vector3 velocity = Vector3.zero;

        // Spherecast instead of ray cast, using ray casts can miss possible collisions and was giving me issues on occasion.
        RaycastHit hit = new RaycastHit();
        bool collisionResult = Physics.SphereCast(transform.position + transform.up * 0.1f, charController.radius, -transform.up, out hit, 10f, layerMask, QueryTriggerInteraction.Ignore);
        // Debug ray drawn to the collision point. Helps when the ray doesn't intersect anything to 'help'(i.e. irritate me)  detect errors.
        Debug.DrawLine(transform.position, hit.point, Color.green);

        if (collisionResult)
        {
            // Project the camera forward onto the plane to get our desired forward direction. Note that this could cause
            // issues if the cameras forward direction is pointing exactly along the up direction of the character. Maybe 
            // i'll try and fix this later.
            Vector3 projectedForward = Vector3.ProjectOnPlane(Camera.main.transform.forward, hit.normal);
            projectedForward.Normalize();

            // Ray to test what the hell is going on with the projected forward...just incase it misbehaves....
            Debug.DrawLine(hit.point, hit.point + projectedForward, Color.yellow);

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(projectedForward, hit.normal), 10f * Time.deltaTime);
            velocity = projectedForward * (y * movementSpeed) + transform.right * (x * movementSpeed);

            // Stick to surface.
            if (collisionResult && hit.distance < 5f)
                transform.position = hit.point + transform.up * ((charController.height / 2f) + 0.01f);
        }
        else
        {
            Debug.Log("ERROR!!! : There was no collision detected!!! WHAT-THE-HELL!!!!");
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(transform.forward, Vector3.up), 10f * Time.deltaTime);
        }

        charController.Move(velocity);
    }
}

After a little bit more tinkering i was able to come away with this, give it a shot. You will have to customize it to
suit your own needs and i still feel it works best when the camera rotates with the character but it works ok all things consider. Hopefully it helps :stuck_out_tongue: best a scrub like me can do for ya. Good luck!

using UnityEngine;

[RequireComponent(typeof(CharacterController))]
public class TimsSimpleMotor : MonoBehaviour
{
    [Tooltip("The speed of our motor.")]
    public float movementSpeed = 5f;
    [Tooltip("The layer mask for our ground detector ray.")]
    public LayerMask layerMask;

    CharacterController charController;

    Vector3 savedContactNormal = Vector3.up;

    // Use this for initialization
    void Start ()
    {
        charController = GetComponent<CharacterController>();
    }

    // Update is called once per frame
    void Update ()
    {
        float x = Input.GetAxis("Horizontal") * Time.deltaTime;
        float y = Input.GetAxis("Vertical") * Time.deltaTime;
      
        Vector3 movement = new Vector3(x, 0, y);
        movement.Normalize();

        movement = Camera.main.transform.TransformDirection(movement);
        Debug.DrawLine(transform.position, transform.position + movement * 10f, Color.black);

        Vector3 velocity = Vector3.zero;

        // Spherecast instead of ray cast, using ray casts can miss possible collisions and was giving me issues on occasion.
        RaycastHit hit = new RaycastHit();
        bool collisionResult = Physics.SphereCast(transform.position + transform.up * 0.1f, charController.radius, -transform.up, out hit, 10f, layerMask, QueryTriggerInteraction.Ignore);
        // Debug ray drawn to the collision point. Helps when the ray doesn't intersect anything to 'help'(i.e. irritate me)  detect errors.
        Debug.DrawLine(transform.position, hit.point, Color.green);

        if (movement.magnitude > 0.1f && collisionResult)
        {
            savedContactNormal = hit.normal;

            Quaternion yMatchSurfaceRotation = Quaternion.FromToRotation(transform.up, hit.normal);
            movement = Quaternion.FromToRotation(Camera.main.transform.up, hit.normal) * movement;
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(movement, hit.normal), 5f * Time.deltaTime);

            Debug.DrawLine(transform.position, transform.position + movement * 10f, Color.gray);

            velocity = transform.forward * movementSpeed * Time.deltaTime;

            // Stick to surface.
            if (hit.distance < 5f)
                transform.position = Vector3.Lerp(transform.position, hit.point + transform.up * ((charController.height / 2f) + 0.01f), 10f * Time.deltaTime);
        }
        else
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(transform.forward, savedContactNormal), 10f * Time.deltaTime);

        charController.Move(velocity);
    }
}

When you want to rotate relative to the world you affect transform.rotation. however when you want to rotate relative to an objects orientation you should affect transform.localRotation instead.

Your surface normal calcuations are done in world space so they should use rotation, yet you want your character’s rotation from input to be done in localspace so you should use localRotation with your inputs.

LocalRotation is relative to the parent object’s rotation. My character controller has no parent gameobject.

Here’s my entire script if anyone can make any sense of it and fix it lol:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour {
    public bool grounded;
    public Animator anim;
    public Rigidbody rb;
    public GameObject forward;
    public bool jump;
    public bool jumpActivated;
    public AudioClip jumpSFX;
    public GameObject rotOBJ;
    public bool inHouse;

    void Start () {
        grounded = true;
        rb = GetComponent<Rigidbody> ();
    }

    void FixedUpdate () {
        anim.SetFloat ("MoveSpeed", (Mathf.Lerp (anim.GetFloat ("MoveSpeed"), rb.velocity.magnitude, Time.deltaTime * 7f)));
    }

    void Update () {
        RaycastHit hit;
        Vector3 pos = transform.position + transform.up * 0.1f;
        if (Physics.Raycast (pos, -transform.up, out hit, 0.3f)) {


            transform.rotation = Quaternion.Slerp (transform.rotation, Quaternion.FromToRotation (transform.up, hit.normal) * transform.rotation, Time.deltaTime * 15f);
      
            if (jumpActivated == false) {
                if (Vector3.Dot (transform.forward, rb.velocity) > 4f) {
                    transform.position = new Vector3 (transform.position.x, hit.point.y, transform.position.z);
                }
            }
        }

        RaycastHit[] cast = Physics.RaycastAll (transform.position + transform.up * 0.6f, -transform.up, 0.75f);
        int idx = 0;
        grounded = false;
        while (idx < cast.Length) {
            if (cast [idx].collider.gameObject.tag == "Floor") {
                grounded = true;
            }
            idx++;
        }

        float myAngle = Mathf.Atan2 (Input.GetAxis ("Horizontal"), Input.GetAxis ("Vertical")) * Mathf.Rad2Deg;

        float bodyRotation = myAngle + forward.transform.rotation.y;

        Vector2 inputPower = new Vector2 (Input.GetAxis ("Horizontal"), Input.GetAxis ("Vertical"));

        Quaternion originalRot = transform.rotation;

        Vector3 newrot = Vector3.Scale (new Vector3 (0, bodyRotation, 0), transform.up);

        if (inputPower.magnitude > 0.1) {
            transform.localRotation = Quaternion.Euler (newrot);
        }

        float vel;
        if (inHouse == true) {
            vel = Mathf.Lerp (Vector3.Dot (transform.forward, rb.velocity), inputPower.magnitude * 10f, Time.deltaTime * 3f);
        } else {
            vel = Mathf.Lerp (Vector3.Dot (transform.forward, rb.velocity), inputPower.magnitude * 35f, Time.deltaTime * 1.5f);
        }

        if (grounded == true) {
            rb.velocity = transform.forward * vel;

        } else {
            transform.rotation = Quaternion.Slerp (transform.rotation, new Quaternion (0, originalRot.y, 0, originalRot.w), Time.deltaTime * 5f);
            rb.AddForce (-Vector3.up * 25f);
            rb.AddForce (transform.forward * inputPower.magnitude * 25f);
        }
    }
}