Game Math: Inverse Trigonometric Functions, Slope Angles, And Facing Objects

Source files and future updates are available on Patreon.
You can follow me on Twitter.

This post is part of my Game Math Series.

本文之中文翻譯在此

Prerequisites

Overview

At this point, we have learned about the three basic trigonometric functions: sine, cosine, and tangent. Now, we are going to take a look at their inverse functions, as well as how they can be utilized in games.

In this tutorial, you’ll learn:

  • The inverse functions of the three basic trigonometric functions.
  • How to compute the angle of a slope given a desired slope value.
  • The domains and ranges of inverse trigonometric functions.
  • The special convenience inverse trigonometric function atan2.
  • How to make an object face towards the mouse cursor.

Inverse Functions

A function can be treated like a black box that takes some input and gives you some output. If a function f takes an input x and spits out an output y, we can write it as y = f(x) (read y equals f of x). Meanwhile, if a function can take an output of f and give back what input f takes that could produce such output, we say that such function is the inverse of f, and we write it as f^{-1} (read f inverse).

In other words, if the function f takes x and gives you y, which can be written as y = f(x), then f^{-1} can take y as input and give you x, which can be written as x = f^{-1}(y).

An example of a function verses its inverse is a function that adds one to its input and a function that subtracts one from its input. Let Add1(x) denote the function that adds one to x, and Sub1(y) denote the one that subracts one from y. If we feed x=2 into Add1(x), we get:

     \begin{flalign*} y = Add1(2) = 3 \end{flalign*}

Now, if we feed y=3 back into Sub1(y), we get our original input x=2 back:

     \begin{flalign*} x = Sub1(3) = 2 \end{flalign*}

Inverse Trigonometric Functions

We already know that trigonometric functions take an angle as input and produce a number as output. We can feed the output of a trigonometric function (a real number) into its inverse function, and the inverse function would spit out the original input to the trigonometric function (an angle in radians). For example, \sin\frac{\pi}{2} = 1, and \sin^{-1}1 = \frac{\pi}{2}.

Inverse trigonometric functions have special names. Rather than “sine inverse”, the inverse of sine, written as \sin^{-1}, is called arcsine. Similarly, cos^{-1} and tan^{-1} are called arccosine and arctangent, respectively. In Unity, here’s how you’d call these three inverse trigonometric functions:

float sinAngle = Mathf.Asin(sinValue); // arcsine
float cosAngle = Mathf.Acos(cosValue); // arccosine
float tanAngle = Mathf.Atan(tanValue); // arctangent

Slope Angles

As a quick example, if we know the ratio of vertical rise versus horizontal offset of a hill in a game level, how do we compute the angle of the slope? Using the illustration below, how do we compute \theta from the vertical rise V and horizontal offset H?

The goal is to express \theta using V and H. First, we can relate \theta to V and H using the tangent function:

     \begin{flalign*} \tan\theta = \frac{V}{H} \end{flalign*}

Next, we can obtain \theta by feeding \frac{V}{H} into \tan^{-1}:

     \begin{flalign*} \theta = \tan^{-1}\frac{V}{H} \end{flalign*}

Alternatively, we can view the equation above as the result of taking the arctangent of both sides of the previous equation. Generally, f^{-1}(f(x)) cancels out and gives you x; similarly, f(f^{-1}(x)) cancels out and gives you y.

The angle \theta is in radians. As mentioned in an earlier tutorial, we can convert the angle’s unit to degrees by multiplying it with \frac{180}{\pi}.

So, we can make a little interactive program that allows the user to move a point that forms a slope with the origin, and use the point’s coordinates (X, Y) to compute and display the slope angle.

And here’s the code:

Vector3 point = p.transform.position;

// compute slope angle in radians
float angleRad = Mathf.Atan(point.y / point.x);

// convert to degrees
// Mathf.Rad2Deg is a constant equal to 180.0f / Pi
float angleDeg = angleRad* Mathf.Rad2Deg;

text = angleDeg + "°";

Domains And Ranges

When using inverse trigonometric functions, it’s important to understand their domains and ranges.

The domain of a function is the collection of all valid values as input, and the range of a function is the collection of all possible output values.

For example, the domain of \sin\theta is the collection of all real numbers, because you can pass any angle to it as input. And the range of \sin\theta is [-1, 1], which is a notation for the collection including all values between and including -1 and 1. If a parenthesis is used instead of a bracket, it means that side of the boundary is not included in the collection; for example, [0, 10) denotes a collection including all values between 0 and 10, but only including the boundary 0 and not the boundary 10.

The inverse of a function should simply have a domain and range equal to the range and domain, respectively, of the corresponding function, right? For inverse trigonometric functions, that’s not the case.

Trigonometric functions are periodic, which means multiple different input values can result in the same output value. For \sin\theta and \cos\theta, they even can have different input values within a single period resulting in the same output value.

Let’s use \sin\theta as an example again. Both \sin\frac{\pi}{2} and \sin\frac{5\pi}{2} give the same value 1. So what is the output of \sin^{-1}1? It can’t be simultaneously equal to \frac{\pi}{2}, \frac{5\pi}{2}, or other inputs that make \sin\theta equal to 1. In fact, the ranges of inverse trigonometric functions are chosen to be of limited range, commonly agreed upon and universally used.

Since the ranges of \sin\theta and \cos\theta are both [-1, 1], the domains of \sin^{-1}x and \cos^{-1}x are both [-1, 1] as well. The ranges of \sin^{-1}x and \cos^{-1}x are chosen to be [\frac{-\pi}{2}, \frac{\pi}{2}] and [0, \pi], respectively. These ranges cover an angle range of \pi radians, or 180 degrees.

Hence, \sin^{-1}1 is equal to \frac{\pi}{2}, which is the one and only input that makes \sin\theta equal to 1 and lies within the range [\frac{-\pi}{2}, \frac{\pi}{2}].

As for \tan^{-1}x, since the range of \tan\theta is the collection of all real numbers, the domain of \tan^{-1}x is the collection of all real numbers as well. And the range of \tan^{-1}x is chosen to be [\frac{-\pi}{2}, \frac{\pi}{2}], same as that of \sin^{-1}x.

The Atan2 Convenience Function

Lets say we have a point P=(P_x, P_y) in 2D, and it is in the first quadrant, i.e. P_x > 0 and P_y > 0. Let \theta be the angle from the +x axis to the line segment connecting the origin and P.

We know that \tan\theta = \frac{P_y}{P_x}, so we can compute \theta from the coordinates of P using the arctangent function: \theta = \tan^{-1}\frac{P_y}{P_x}. Since both P_x and P_y are positive, \theta would lie within [0, \frac{\pi}{2}], encompassed within the full range of arctangent, which is [-\frac{\pi}{2}, \frac{\pi}{2}].

This this is what the computation looks like in code:

float angle = Mathf.Atan(p.y / p.x);

What if P is in the fourth quadrant, i.e. P_x > 0 and P_y < 0? \frac{P_y}{P_x} would become negative and \tan^{-1}\frac{P_y}{P_x} would output a negative angle within [\frac{-\pi}{2}, 0], which is also encompassed within the full range of arctangent, [-\frac{\pi}{2}, \frac{\pi}{2}].

Problems arise when we have P in the second or third quadrant. If P is in the second quadrant, i.e. P_x < 0 and P_y > 0, the fraction \frac{P_y}{P_x} is negative. We can find a point P_2=(P_{2x}, P_{2y}) in the second quadrant that results in a ratio \frac{P_{2y}}{P_{2x}} equal to a ratio \frac{P_{4y}}{P_{4x}} from a point P_4=(P_{4x}, P_{4y}) in the fourth quadrant. One such point pair are those that satisfy (P_{2x},  P_{2y}) = (-P_{4x},  -P_{4y}).

The two points P_2 and P_4 in the figure below have identical coordinate ratios \frac{P_{2y}}{P_{2x}} = \frac{P_{4y}}{P_{4x}}.

Also seen in the figure above is that the coordinate ratios of points P_2 and P_4, when compared to the coordinate ratios of points P_1 in the first quadrant and P_3 in the third quadrant, only differ in signs (negative instead of positive). All the absolute sharp angles (angles less than 90 degrees) between the line segments connecting the origin & the points and the X axis are identical.

The ratio \frac{P_{2y}}{P_{2x}} is equal to the ratio \frac{-P_{4y}}{-P_{4x}}, which is in turn equal to \frac{P_{4y}}{P_{4x}}, because the two negative signs cancel out. So, if we pass \frac{P_{2y}}{P_{2x}} as input to the arctangent function, \tan^{-1}\frac{P_{2y}}{P_{2x}} actually gives you the same negative angle as \tan^{-1}\frac{P_{4y}}{P_{4x}}, because an angle in the fourth quadrant is within the range of arctangent, but an angle in the second quadrant is not.

When we pass in \frac{P_{2y}}{P_{2x}} to the arctangent function, what we really want to get is the green positive astute angle (angle larger than 90 grees) shown in the figure below, not the red negative sharp ones. We always want to start measuring angles from the +X direction.

In order to do so, before combining P_{x} and P_{y} into a ratio and passing it to the arctangent function, we check the signs of P_x and P_y first to see which quadrant the point is in. And if we get an angle outside the range [-\frac{\pi}{2}, \frac{\pi}{2}], we fix up the output of the arctangent function to get the output angle in the correct quadrant. Here’s the code that does this fix-up:

// range of this function is (-pi, pi]
float FixedUpAtan(float py, float px)
{
  if (px &gt; 0.0f) // normal, no fix-up needed
  {
    // &amp;amp;amp;quot;normal&amp;amp;amp;quot;
    // py &gt; 0.0f : first quadrant
    // py &lt; 0.0f : fourth quadrant
    return Mathf.Atan(py / px);
  }
  else if (px &lt; 0.0f) // fix-up needed
  {
    if (py &gt; 0.0f) // second quadrant
      return Math.PI + Mathf.Atan(py / px);
    else if (py &lt; 0.0f) // third quadrant
      return -Math.PI + Mathf.Atan(py / px);
    else // angle on negative X axis
      return 2.0f * Mathf.PI;
  }
  else // infinity
  {
    if (py &gt; 0.0f)
      return 0.5f * Mathf.PI; // ratio is positive infinity
    else if (py &lt; 0.0f)
      return -0.5f * Mathf.PI; // ratio is negative infinity
    else
      return 0.0f; // degenerate input (the origin)
  }
}

That seems like quite a lot of work. Luckily, almost all standard math libraries in any programming languages provide a convenience function called atan2, which has a full 360-degree range of (-\pi, \pi] and does exactly what the code above does (most likely in a more efficient and optimized fashion). Note that the argument order is Y first and X second. Atan2 in different libraries may have different ordering of the two arguments, but based on what I’ve seen, Y followed by X is pretty common.

I often see a misconception that atan2 is just an alternative to the arctangent function and doesn’t do anything extra that arctangent cannot do. This is actually incorrect. The arctangent function only takes a single value as input, and its output range is [\frac{-\pi}{2}, \frac{\pi}{2}]. On the other hand, atan2 takes two values as input (P_y and P_x before they are combined into a single ratio), and the output has a full 360-degree range of (-\pi, \pi].

Facing An Object Towards The Mouse Cursor in 3D

Lastly, let’s look at a classic example of facing an object towards the mouse cursor.

First, find the intersection between the ray under the mouse cursor and the ground plane. Then, place an object at that intersection, creating the effect of the object following the mouse cursor in 3D. This object is our look target.

Camera cam = Camera.current;
Vector3 mouse= Input.mousePosition;
Ray ray = cam.ScreenPointToRay(mouse);
float rayDist;
plane.Raycast(ray, out rayDist);
sphere.position = ray.GetPoint(rayDist);

Next, let’s use our old friend UFO Bunny from Boing Kit again. When un-rotated, her forward vector is in the +X direction, and her left vector is in the +Z direction. We want to face her towards the look target.

Then, let UFO Bunny be the origin, and calculate the coordinates of the look target relative to her:

Vector3 coord =  
  sphere.transform.position 
  - ufoBunny.transform.position;

Now, let’s mark up the scene with an angle \theta between the X axis and the line segment connecting UFO Bunny and the look tartget:

As shown before, the angle \theta can be calculated from the convenient atan2 function:

float thetaRad = Mathf.atan2(coord.z, coord.x); // in radians

Recall this figure:

This figure shows the XY plane, and as \theta increases, P rotates counterclockwise around the origin. The rotation axis of such rotation is the +Z axis (later tutorials will explain this in more details). The UFO Bunny and the look target lie on the XZ plane; to translate the figure on the XY plane to the XZ plane, we map the +X axis to the +X axis, the +Y axis to the +Z axis, and the rotation axis of the +Z axis to the -Y axis.

Now that we have the rotation axis and the desired rotation angle, we can finally construct a quaternion representing such rotation. Quaternions will also be covered in later tutorials. For now, we just need to know that quaternion is a type of data Unity uses to represent object rotation.

float thetaDeg = thetaRad * Mathf.Rad2Deg; // in degrees
float axis = Vector3.down; // (0, -1, 0) == -Y axis
Quaternion rot = Quaternion.AngleAxis(thetaDeg, axis);
ufoBunny.transform.rotation = rot;

And here’s our final result:

Note: Unity already provides helper functions like Quaternion.LookRotation and Transform.LookAt that can achieve the same effect. But the purpose of this tutorial is to help understand inverse trigonometric functions.

Summary

In this tutorial, we have been introduced to the inverse trigonometric functions, how they relate to their corresponding trigonometric functions, and their domains and ranges.

Also, we have seen that the arctangent function doesn’t have a full 360-degree range, but a convenient utility function atan2 does.

Lastly, we have learned how to use the atan2 function to implement the classic example of facing an object towards the mouse cursor.

If you enjoyed this tutorial and would like to see more, please consider supporting me on Patreon. By doing so, you can also get updates on future tutorials. Thanks!

About Allen Chou

Physics / Graphics / Procedural Animation / Visuals
This entry was posted in Gamedev, Math, Unity. Bookmark the permalink.

1 Response to Game Math: Inverse Trigonometric Functions, Slope Angles, And Facing Objects

  1. KB says:

    I’m learning about Inverse Functions in school! Very cool to see them applied in programming.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.