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 takes an input and spits out an output , we can write it as (read y equals f of x). Meanwhile, if a function can take an output of and give back what input takes that could produce such output, we say that such function is the inverse of , and we write it as (read f inverse).
In other words, if the function takes and gives you , which can be written as , then can take as input and give you , which can be written as .
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 denote the function that adds one to , and denote the one that subracts one from . If we feed into , we get:
Now, if we feed back into , we get our original input back:
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, , and .
Inverse trigonometric functions have special names. Rather than “sine inverse”, the inverse of sine, written as , is called arcsine. Similarly, and 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 from the vertical rise and horizontal offset ?
The goal is to express using and . First, we can relate to and using the tangent function:
Next, we can obtain by feeding into :
Alternatively, we can view the equation above as the result of taking the arctangent of both sides of the previous equation. Generally, cancels out and gives you ; similarly, cancels out and gives you .
The angle is in radians. As mentioned in an earlier tutorial, we can convert the angle’s unit to degrees by multiplying it with .
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 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 is the collection of all real numbers, because you can pass any angle to it as input. And the range of is , 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, 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 and , they even can have different input values within a single period resulting in the same output value.
Let’s use as an example again. Both and give the same value 1. So what is the output of ? It can’t be simultaneously equal to , , or other inputs that make 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 and are both , the domains of and are both as well. The ranges of and are chosen to be and , respectively. These ranges cover an angle range of radians, or 180 degrees.
Hence, is equal to , which is the one and only input that makes equal to 1 and lies within the range .
As for , since the range of is the collection of all real numbers, the domain of is the collection of all real numbers as well. And the range of is chosen to be , same as that of .
The Atan2 Convenience Function
Lets say we have a point in 2D, and it is in the first quadrant, i.e. and . Let be the angle from the axis to the line segment connecting the origin and .
We know that , so we can compute from the coordinates of using the arctangent function: . Since both and are positive, would lie within , encompassed within the full range of arctangent, which is .
This this is what the computation looks like in code:
float angle = Mathf.Atan(p.y / p.x);
What if is in the fourth quadrant, i.e. and ? would become negative and would output a negative angle within , which is also encompassed within the full range of arctangent, .
Problems arise when we have in the second or third quadrant. If is in the second quadrant, i.e. and , the fraction is negative. We can find a point in the second quadrant that results in a ratio equal to a ratio from a point in the fourth quadrant. One such point pair are those that satisfy .
The two points and in the figure below have identical coordinate ratios .
Also seen in the figure above is that the coordinate ratios of points and , when compared to the coordinate ratios of points in the first quadrant and 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 is equal to the ratio , which is in turn equal to , because the two negative signs cancel out. So, if we pass as input to the arctangent function, actually gives you the same negative angle as , 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 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 and into a ratio and passing it to the arctangent function, we check the signs of and first to see which quadrant the point is in. And if we get an angle outside the range , 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 > 0.0f) // normal, no fix-up needed { // &amp;amp;quot;normal&amp;amp;quot; // py > 0.0f : first quadrant // py < 0.0f : fourth quadrant return Mathf.Atan(py / px); } else if (px < 0.0f) // fix-up needed { if (py > 0.0f) // second quadrant return Math.PI + Mathf.Atan(py / px); else if (py < 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 > 0.0f) return 0.5f * Mathf.PI; // ratio is positive infinity else if (py < 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 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 . On the other hand, atan2 takes two values as input ( and before they are combined into a single ratio), and the output has a full 360-degree range of .
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 between the X axis and the line segment connecting UFO Bunny and the look tartget:
As shown before, the angle 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 increases, 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!
I’m learning about Inverse Functions in school! Very cool to see them applied in programming.