Calculating the "slope" of a polygon (math problem).

Okay so I’m making a 3rd Person Camera and I want to be able to detect the slope of the terrain (how steep it is) to check if the character is able to move up the slope or if he should slide. To do this I want to calculate the angle between the polygon-face and the “world-up vector” (removing 90 degrees from that is also needed) so that I can set a max angle that you can walk on. To calculate this angle I need to know the vector along the face of the polygon, perpendicular to the normal with a direction down the slope (image below). I know how to do the calculations after I have that vector. The calculations I want do do is as follows:

var slopeAngleRadians = Mathf.Acos(Vector3.Dot(Vector3.Normalize(A), Vector3.up));
var slopeAngleDegrees = slopeAngleRadians * 180 / Mathf.PI;
var slopeAngleFinal = Mathf.Abs(slopeAngleDegrees - 90);

The problem here is the vector A which is the one I don’t know how to calculate. I include a picture of it here so you understand my problem better (I know the normal of the polygon and X is the angle I want to calculate). Click on picture to enlarge:

Thanks in advance.

ControllerColliderHit gives you information about what is being hit. Including hit.normal

So you can see if its too steep or not with somethin like this:

private bool Is_TooSteep () {
return (hit.normal.y <= Mathf.Cos (SLOPELIMITOFYOURCHOICE * Mathf.Deg2Rad));
}

and obviously you can get slope normal with:

Vector3 slopeDirection = new Vector3 (hit.normal.x, 0.0f, hit.normal.z).normalized;

etc.

Tried something like that but I never got it to work. Gonna try with your code and see what happens.

Edit:

hit.normal.y <= Mathf.Cos (SLOPELIMITOFYOURCHOICE * Mathf.Deg2Rad)

That worked except I had to reverse from “<=” to “>=”, however as the sloops get steeper, the sliding isn’t smooth but kind of jagged. Dont know why but it feels like the sliding is happening in intervals making the camera jump to follow in small increments.

Edit 2:

Now I notice its not working as expected att all. If I use “<=” the character gets stuck and cant move at all, and if I reverse it everything gets…reveresed :slight_smile: When I use steep angles (80-90 degrees) I can move up the slope but when I have low angles I cant walk up the slope and there is yet another problem. If the slope isnt that steep I can still move up but with very jagged movements (remember it was reversed so these “not steep” angles is actually steep so the slope is treated as a steep slope when it actually isnt).

Do you add this bit to your final movement direction?

slopeDirection = new Vector3 (hit.normal.x, 0.0f, hit.normal.z).normalized;

It will push your character controller in the opposite side of direction. You might try to adjust it according to your movement speed though.

Yes but It’s still not working. I decided to instead use a SlideThreshold of 0.9 and a MaxControllableSlideThreshold of 0.4. If the normal is less than SlideThreshold the character should begin to slide (but you should still be able to fight it by moving) and when its below MaxControllable, then its too steep and you wont be able to move upwards or jump. It works all well with these values but if I increase the MaxControllable to lets say 0.89 and walk on a slope with a normal of 0.85, then I can still move up the slope (with very jagged movements) even though im above the MaxControllableSlide. I include the code here:

        if (Physics.Raycast(transform.position, Vector3.down, out hitInfo))
        {
            // If the "terrain" is steep enough, create a slide direction from the normal
            if (hitInfo.normal.y < SlideThreshold)
            {
                slideDirection = new Vector3(hitInfo.normal.x, -hitInfo.normal.y, hitInfo.normal.z);
            }
        }

        // Check if the "terrain" is too steep to be able to control the slide
        if (hitInfo.normal.y > MaxControllableSlideMagnitude)
        {
            MoveVector += slideDirection;
            UncontrolledSlide = false;
        }
        else
        {
            MoveVector = slideDirection;
            UncontrolledSlide = true;
        }

It seems that it doesn’t like “uncontrollable” slides on “not so steep” surfaces.

Well, i didnt mess with character movement since a long time. But here i found some of my old and very messy code if it helps.
It wont work straight away, you need to delete refferences to Core class and might need to do some more deleting here and there. This is just for your refference.

CharController.cs

using UnityEngine;
using System.Collections;

[System.Serializable]
public class CharController_Motion {
	//Public Variables
	//Walking
	public float walkSpeed = 2.0f;
	public float runSpeed = 4.0f;
	public float slideSpeed = 5.0f;
	//Jumping
	public float jumpHeight = 1.0f;
	public int jumpTimerDelay = 15;
	//Falling
	public float maxFallDistance = 15.0f;
	//Others
	public float gravity = 20.0f;
	//Private Variables
	internal int jumpTimer;
	internal bool isFalling = false;
	internal float fallBegin = 0.0f;
	internal float fallEnd = 0.0f;
	internal float fallDistance = 0.0f;
	internal Vector3 direction = Vector3.zero;
	internal Vector3 hitPoint = Vector3.zero;
}

[System.Serializable]
public class CharController_Physics {
	//Public Variables
	public float pushPower = 2.0f;
	public LayerMask pushLayers = -1;
}

[System.Serializable]
public class CharController_InputManager {
	//Private Variables
	internal Vector3 direction = Vector3.zero;
	internal Quaternion rotation = Quaternion.identity;
	internal bool run = false;
	internal bool jump = false;
	internal bool crouch = false;
}

[AddComponentMenu ("Aubergine/Character/CharController")]
[RequireComponent (typeof (CharacterController))]
public class CharController : MonoBehaviour {
	//Class declerations
	public CharController_Motion cc_Motion = new CharController_Motion();
	public CharController_Physics cc_Physics = new CharController_Physics();
	public CharController_InputManager cc_InputManager = new CharController_InputManager();

	//Internal Variables
	internal Transform trs;
	internal CharacterController cc;
	internal Rigidbody rbd;
	internal float ccHeight;

	//Private Variables
	private Vector3 moveDirection;
	private float moveSpeed, jumpSpeed;
	private CollisionFlags collFlags;
	private Vector3 groundNormal;
	
	void Awake () {
		//Get Components
		trs = GetComponent<Transform>();
		cc = GetComponent<CharacterController>();
		rbd = GetComponent<Rigidbody>();
		if (rbd) rbd.freezeRotation = true;
	}

	void Start () {
		//Init class declerations
		cc_Motion.jumpTimer = cc_Motion.jumpTimerDelay;
		//Init private variables
		moveDirection = Vector3.zero;
		moveSpeed = cc_Motion.walkSpeed;
		jumpSpeed = cc_Motion.jumpHeight;
		collFlags = CollisionFlags.None;
		groundNormal = Vector3.zero;
		ccHeight = cc.height;
		//ccCenter = cc.center;
	}

	void FixedUpdate () {
		if (Core.App.paused)
			return;
		//Rotation
		trs.rotation = cc_InputManager.rotation;
		//Motion
		if (cc.isGrounded) {
			//Get world direction and speed of motion
			moveDirection = trs.TransformDirection (cc_InputManager.direction);
			moveSpeed = cc_InputManager.run ? cc_Motion.runSpeed : cc_Motion.walkSpeed;
			moveDirection *= moveSpeed;
			//Push down to ground for consistent isGrounded check
			float groundPush = Mathf.Max(cc.stepOffset, new Vector3(moveDirection.x, 0, moveDirection.z).magnitude);
			moveDirection -= groundPush * Vector3.up;
			//Slide down hills
			if (Is_TooSteep ()) {
				Vector3 slopeDirection = new Vector3 (groundNormal.x, 0.0f, groundNormal.z).normalized;
				//Vector3 projectedDirection = Vector3.Project(moveDirection, slopeDirection);
				//moveDirection = (slopeDirection + projectedDirection + (moveDirection - projectedDirection)) * cc_Motion.slideSpeed;
				moveDirection += slopeDirection * cc_Motion.slideSpeed;
				//moveDirection.y -= cc_Motion.gravity * Time.deltaTime;
			}
			//Crouching
			if (cc_InputManager.crouch  cc_Motion.jumpTimer > 0) {
				if (cc.height != ccHeight * 0.5f) {
					cc.height = ccHeight * 0.5f;
					cc.center = new Vector3(0.0f, 0.5f, 0.0f);
				}
			}
			else {
				if (cc.height != ccHeight) {
					cc.height = ccHeight;
					cc.center = new Vector3(0.0f, 1.0f, 0.0f);
				}
			}
			//Jumping
			if (cc_InputManager.jump  cc_Motion.jumpTimer >= cc_Motion.jumpTimerDelay  !Is_TouchingCeiling ()) {
				//Dont crouch in air
				if (cc.height != ccHeight) {
					cc.height = ccHeight;
					cc.center = new Vector3(0.0f, 1.0f, 0.0f);
				}
				//Keep jumping
				cc_Motion.jumpTimer = 0;
				jumpSpeed = cc_InputManager.crouch ? cc_Motion.jumpHeight * 2.0f : cc_Motion.jumpHeight;
				moveDirection.y = Mathf.Sqrt (2.0f * jumpSpeed * cc_Motion.gravity);
			}
			else {
				cc_Motion.jumpTimer++;
				cc_Motion.jumpTimer = Mathf.Clamp (cc_Motion.jumpTimer, 0, cc_Motion.jumpTimerDelay);
			}
			//Falling
			if (cc_Motion.isFalling) {
				cc_Motion.isFalling = false;
				cc_Motion.fallDistance = cc_Motion.fallBegin - trs.position.y;
				if (cc_Motion.fallDistance > cc_Motion.maxFallDistance)
					SendMessage("ApplyDamage", 10);
					//ApplyFallDamage (cc_Motion.fallDistance);
			}
		}
		else {
			if (Is_TouchingCeiling ()) {
				moveDirection.y -= cc_Motion.gravity * Time.deltaTime;
			}
			//Get fall begin position
			if (!cc_Motion.isFalling) {
				cc_Motion.isFalling = true;
				cc_Motion.fallBegin = trs.position.y;
			}
			//Apply gravity
			moveDirection.y -= cc_Motion.gravity * Time.deltaTime;
		}
		//Move the character
		collFlags = cc.Move (moveDirection * Time.deltaTime);
	}

	void OnControllerColliderHit (ControllerColliderHit hit) {
		if (Core.App.paused)
			return;
		//Check hit body info
		groundNormal = (hit.collider.GetType() == (typeof (BoxCollider)) ? Vector3.up : hit.normal);
		cc_Motion.hitPoint = hit.point;
		//Pushing rigidbodies
		Rigidbody hitBody = hit.collider.attachedRigidbody;
		if (hitBody == null || hitBody.isKinematic) return;
		LayerMask hitBodyLayerMask = 1 << hitBody.gameObject.layer;
		if ((hitBodyLayerMask  cc_Physics.pushLayers.value) == 0) return;
		if (hit.moveDirection.y < -0.3f) return;
		Vector3 pushDirection = new Vector3 (hit.moveDirection.x, 0.0f, hit.moveDirection.z);
		hitBody.velocity = pushDirection * cc_Physics.pushPower;
	}

	private bool Is_TooSteep () {
		return (groundNormal.y <= Mathf.Cos (cc.slopeLimit * Mathf.Deg2Rad));
	}

	private bool Is_TouchingCeiling () {
		return (collFlags  CollisionFlags.Above) != 0;
	}

	private void ApplyFallDamage (float fallDistance) {
		Debug.Log ("Fall distance: " + fallDistance);
	}
}

CharInput.cs

using UnityEngine;
using System.Collections;

[AddComponentMenu ("Aubergine/Character/CharInput")]
[RequireComponent (typeof (CharController))]
[RequireComponent (typeof (CharInteractor))]
public class CharInput : MonoBehaviour {
	//Public Variables
	public Transform fpsCamera;
	public float cameraMaxHeadAngle = 80.0f;
	public float mouseSensitivity = 10.0f;
	//Private Variables
	private CharController controller;
	private CharInteractor interactor;
	private CharacterController cc;
	private Vector3 direction;
	private Quaternion qBody, qCamera;
	private float directionLength, xRotation, yRotation;

	void Awake () {
		controller = GetComponent<CharController>();
		interactor = GetComponent<CharInteractor>();
		cc = GetComponent<CharacterController>();
		direction = Vector3.zero;
		qBody = transform.rotation;
		qCamera = fpsCamera.localRotation;
		directionLength = xRotation = yRotation = 0.0f;
	}

	void Update () {
		if (Core.App.paused)
			return;
		//Get Direction
		direction = new Vector3 (Input.GetAxis ("Strafe"), 0.0f, Input.GetAxis ("Walk"));
		if (direction != Vector3.zero) {
			directionLength = direction.magnitude;
			direction = direction / directionLength;
			//Analog joystick stuff
			directionLength = Mathf.Min(1, directionLength);
			directionLength = directionLength * directionLength;
			direction = direction * directionLength;
		}
		//Get Rotation
		xRotation += Input.GetAxis("Mouse X") * mouseSensitivity;
		xRotation = AngleWrap(xRotation, -360, 360);
		yRotation -= Input.GetAxis("Mouse Y") * mouseSensitivity;
		yRotation = AngleWrap(yRotation, -cameraMaxHeadAngle, cameraMaxHeadAngle);
		//Pass values to character controller  interactor
		controller.cc_InputManager.rotation = qBody * Quaternion.AngleAxis (xRotation, Vector3.up);
		controller.cc_InputManager.direction = direction;
		controller.cc_InputManager.run = Input.GetButton ("Run");
		controller.cc_InputManager.jump = Input.GetButton ("Jump");
		controller.cc_InputManager.crouch = Input.GetButton ("Crouch");
		interactor.interact = Input.GetButton ("Interact");
		//Adjust position if crouching
		fpsCamera.localPosition = new Vector3 (0.0f, cc.height - 0.1f, 0.0f);
		//Rotate Camera Individually
		fpsCamera.localRotation = qCamera * Quaternion.AngleAxis (yRotation, Vector3.right);
	}
	
	//Wraps angle around
	public static float AngleWrap (float v, float min, float max) {
		if (v < -360.0f) v += 360.0f;
		if (v > 360.0f) v -= 360.0f;
			return Mathf.Clamp(v, min, max);
	}
}