Now You’re Just Projecting
Learning to love Vector3.ProjectOnPlane while making a game about being Godzilla with a pair of chopsticks
Who hasn’t ever wanted to be Godzilla? Marching through the streets of terrified city. Smashing buildings. Grabbing tanks with a pair of chopsticks and popping them into your mouth as they valiantly try to swivel their guns for one last, pointless pot-shot.
While creating my latest VR game, a game where you start off in a sushi cafe and things rapidly take a surreal turn, this is what I wanted to do. Tanks roll in, aim at your face, you eat as many as you can.
As with all of my ventures in Unity 3D, my chosen development platform, this sounded like a simple task. Model up a platonic ideal of a WWII tank. Import it into Unity. Write a simple script to have the Tank’s cab and gun look at the player. Profit.
As with many a Unity developer, or game developer in general, 3D rotations can sometimes be… troublesome. Anyone who tells you they instinctually understand Quaternions is either A)Lying or B)A robot, and you should remind them that you too welcome our new AI overlords.
To help developers overcome their rotational-phobia, Unity provides a range of tools make rotating things easier. the Quaternion static class has several useful functions, including FromToRotation, LookRotation, AngleAxis, all providing safe, Quaternion friendly 3D rotations without having to fiddle with individual components of your object’s Transform.
Rotating the tank’s turret
My tank wanted to look at the player, so LookRotation seemed like the way to go. Let’s take a look at the function.
LookRotation takes in 2 parameters:
- forward - The direction to look in; in my case, the Player’s ugly Godzilla face
- upwards The vector that defines which direction is up; specifically, what is the “up” for your current object
Creating forward was easy enough with a little basic vector math. A vector is just a line that connects two points, and indicates direction. If point T is my tank’s gun turret, and point P is the player, then P minus T is gives me a line connecting the two, with the arrow pointing from the tank to the player.
Giving the function an upwards takes a little more thought.
In Unity Vector3.up gives you the “universal up direction” for the world. In the real world Vector3.up is always pointing toward the straight up towards sky, whether you walking, doing cartwheels, or on a looping roller coaster. If my tank was always going to be on a flat surface, in the natural orientation that a tank wants to be in in, Vector3.up would be the right choice. But Godzillas do not respect the natural orientation of a tank. Godzillas aim to misbehave.
Because I planned to allow the player to pickup the tank, shake it around, toss it in the air and catch it their virtual mouths like a piece of popcorn, I needed a different kind of “up.” Fortunately, Unity provides not only the universal “up” but the objects local “up” in the form of transform.up. This you can think of as the arrow constantly sticking straight up out of your head, and as you rotate, tilt, and nod your head, that “up” still points straight out of your noggin. Since I planned to let my tanks get rolled around, the “up” I wanted was transform.up.
With my forward and upwards defined, I plugged them into Quanternion.LookRotation. If you are veteran Unity 3D developer, you know what’s coming, but let’s look at the results:
Well the tank was looking at me alright, but it forgot that it was tank, and was willing to twist itself into a very non-tank-like fashion to ensure it was pointing straight at the my face.
What I soon came to realize was that while I had specified the right upwards for the LookRotation function, once the “up” is established, the function will align itself with the new forward vector in every way it can, at least in terms of “yaw” and “pitch”.
As brief refresher, in case you need, it, you can think of a 3D rotation as a combination of 3 elements:
Pitch - What your head does when it nods up and down; rotating around the X axis
Yaw - What your head does when shakes side to side; rotating around the Y axis; if you are 3D modeler, you might also know this as “heading”
Roll - What your head does when tilts to the side like a curious puppy; rotating around the Z axis; if you are 3D modeler, you might also know this as “bank”
My problem is that while specifying an upwards prevents the tank’s cab from rolling around its Z axis in a problematic way, it gives back a rotations that includes both pitch around the X axis and yaw around the Y axis. My little tank turret, however, should only be rotating side to side, and not up and down. So how do I tell LookRotation to only rotate in 1 axis? How do I turn my 3D rotation that includes pitch & yaw, into a 2D rotation that only includes yaw?
Well it turns out in the complicated world of 3D vectors, the way you turn something 3D into something 2D is via a projection.
To do a projection, first you need to specify a plane, a 2D square, that will indicate what two dimensions you want to project your 3D information onto. For my case, I want an X-Z plane, or one that is always oriented side to side and front to back so the point I’m telling my tank to look at is on the same elevation and orientation as my tank’s turret. To get that point, imagine taking a flash light pointing directly down toward that plane, and casting a shadow of the player’s head onto that plane. The rotation I wanted would now not be to the player’s head, but to the shadow that the player’s head made on the plane. That’s all a projection is, taking something in 3D space and turning it into something 2D space.
Now it also works out that if the player’s head projected onto that plane is the point my tank should be “looking” at, then the line connecting the tank to the player’s head, when projected onto that plane, is the line connecting the tank to the shadow cast by player’s head on the plane. And that’s what I did.
Unity has a static function in Vector3 called ProjectOnPlane. Let’s take a look:
ProjectOnPlane takes 2 parameters:
- vector - this is the vector that you want to project, the one you are going to shine a flashlight on to see what shadow it makes
- planeNormal- this is the “up” of the plane we are casting the shadow on, so for the tank, a plane that represents side to side and front to back has an “up” that is the same as the tank’s “up” or transform.up
Plugging in the vector that goes from the tank to the player’s head into this function gave me back a new direction to look that would always be on the same level as the tank’s turret, no matter which way I picked up or tossed around the tank. My rotation was restricted to the tank’s yaw or Y axis.
And here was the result:
Aiming the Canon
Now to do the same thing for the tank’s gun, all you have to do is take the lesson learned from rotating the tank’s cab, but use a plane to lock the rotation to up and down, or pitch.
To get that plane, the Y-Z plane that goes front to back, and top to bottom, you need a different “up” vector, or normal vector, that points out from that 2D square. In this case, it’s the gun’s transform.right vector.
Let’s see what happens just copying the same logic as rotating the turret.
Overall not bad, but while a tank’s turret can rotate in 360 degrees, like some kind of angry owl, the canon usually has a limit on both how far up and how far down it can swivel. So let’s set a limit on how much it can rotate up and down.
Restricting a rotation the WRONG way
Sometimes our initial idea might seem perfectly logically, but still end up getting us in trouble. Here is one of many cases where I ignored a warning from Unity and got myself into trouble.
My first thought was to use a function of the quaternions called Euler angles, or the conversion of the quaternion’s rotation into X axis, Y axis, and Z axis components. Thinking I would be clever, I figured I could read the “pitch” or X axis value, use the “clamp” function from Mathf to restrict the value to certain bounds, and create a new quaternion based on those values, and plug it back into the transform
Unity’s documentation has a handy little warning about Quaternions and Euler angles.
While it’s possible to retrieve Euler angles from a quaternion, if you retrieve, modify and re-apply, problems are likely to arise.
Essentially, Unity is saying that what I wanted to do, which was read out the Euler angles from a quaternion, do some math logic, and then plug them back into a quaternion, was a going to result in A BAD TIME.
Another issue with Euler angles is you always get a value between 0–360. That means if you start at 0, rotate up 30 degrees, you get a value between 0–30, but if you rotate down starting from 0 you get a value between 360–330. Now you *could* work with that, do some fancy IF logic to convert that to the range you want, but who has the time for all that?
Restricting a rotating a BETTER way
In order to set a limit on how far the turret has rotated up or down, I first need to be able to compare its current angle to some baseline. I wanted the canon’s initial rotation value, while you might be tempted to just create a variable “initial_rotation” and set it once in the object’s Start() function, since I want to be able to pick up the tank and fling it all around, I’ll want one that will always be the zero state with respect to where the canon is.
A great way to do that in Unity is to just add an empty game object in the same spot as the object you are rotating. That way at any point in the future, you can just poll that object’s position and rotation, and you’ll always have reference point.
So with my reference point established, I just needed to know the angle between my initial reference point, and the current rotation of the canon. There is a very simple static function in the Quaternion class called Angle. Let’s take a look:
Easy enough. Give it two rotations, and it gives you an unsigned angle between them, guaranteed never to be more than 180 degrees. So there’s my logic, whether rotating up or down, if the angle between my initial reference ever exceeds the angle limit I’ve set — in this case 30 degrees — then I halt all rotation. If the angle is less than the limit, then I start rotating toward the player’s head again.
Let’s see it in action:
SHOW ME THE CODE
Let’s take a look at the final code:
Let’s break down the interesting bits.
Update the Reference Point
initialCanonRotation = Canon_initial.transform.rotation;
Since the tank can get picked up and thrown around, we want to make sure the initial reference point is updated every frame. A way to get around that could be to use the localRotation, which only gives you the object’s relative rotation as compared to its parent, but this way will work even if the object is unparented, and is a more universal solution.
Rotating the Turret
//**** Rotate Turret Section ***** //********************************
aimVector = AimPoint.position - turret.position;
aimVector = Vector3.ProjectOnPlane(aimVector, turret.up); newTurretRotation = Quaternion.LookRotation(aimVector, turret.up);Quaternion q_Turret = newTurretRotation; turret.rotation = Quaternion.Slerp(turret.rotation, q_Turret, turret_speed * Time.deltaTime);
The code works like this:
- Set the aimVector, again by subtracting the position of the thing we are rotating from the thing we are rotating towards, giving us a vector pointing from the turret to the Player
- Project the aimVector onto a plane going front to back, side to side, giving us only the side to side, “Yaw,” or Y-axis component of the vector pointing to the player
- Create a new rotation using our projected aimVector by plugging it into Quaternion.LookRotation and using the turret’s up direction so no matter which way the tank gets rolled, all of our rotations start from what would be considered “up” from the turret’s perspective
- The use of a local variable q_Turret to store the Quaternion is not strictly necessary, but if I wanted to limit the turret’s rotation in the future, this would help.
- The last step is simply to set the turret’s rotation to the new rotation we’ve calculated, but by using the Slerp (or Spherical Interpolation) function, we can make the turret rotate at a smooth speed that will naturally ease as it reaches its final rotation. Basically slick animation for free.
Rotating the Canon
//***** Swivel Canon Section ***** //********************************
aimVector = aimPoint.position - canon.position;
aimVector = Vector3.ProjectOnPlane(aimVector, canon.right); newCanonRotation = Quaternion.LookRotation(aimVector, canon.up);Quaternion q_Canon = newCanonRotation;
if (Quaternion.Angle(initialCanonRotation, canon.rotation) >= gun_pitch_limit)
q_Canon = canon.rotation; if (Quaternion.Angle(initialCanonRotation, newCanonRotation) < gun_pitch_limit)
q_Canon = newCanonRotation; canon.rotation = Quaternion.Slerp(canon.rotation, q_Canon, canon_speed * Time.deltaTime);
The code works like this:
- Set the aimVector, again by subtracting the position of the thing we are rotating from the thing we are rotating towards, giving us a vector pointing from the canon to the Player
- Project the aimVector onto an X-Z plane going front to back, top to bottom, giving us only the up and down, “Pitch,” or X-axis rotational component of the vector pointing to the player
- Create a new rotation using our aimVector by plugging it into Quaternion.LookRotation and using the canon’s up direction so no matter which way the tank gets rolled, all of our rotations start from what would be considered “up” from the canon’s perspective.
- Create a temporary variable that we will use as the final rotation, and will help when setting the bounds of how far we want to let the canon rotate up and down.
- FIRST IF LOGIC: Use Quaternion.Angle to get the angle between initialCanonRotation — which is our baseline, unmodified reference transform— and the canon’s current rotation. If the canon has rotated the maximum limit, or slightly past it, then we set our temporary helper variable to whatever canon’s current rotation is, effectively telling the canon to keep it’s current rotation and go no further.
- SECOND IF LOGIC: Use Quaternion.Angle to get the angle between initialCanonRotation — again, the baseline — and the target rotation we’ve calculated. If it’s less the max limit, that means the player is within the canon’s rotation range, so set our temporary helper variable to the rotation we’ve calculated.
- Finally, we use Slerp to assign the rotation to the canon, but in a smooth, slick, pleasantly animated way.
Other ways to rotate
While this was ultimately the way I decided to implement my tank’s rotation logic, this isn’t the only way you can accomplish it.
Another way would be to use AxisAngle, which lets you specify the lone axis you want to build a rotation for, and then specify the angle you want to rotate by. This will require you to have a signed angle (just meaning positive or negative) that you know to create your rotation, and even going down this path, you’ll still want to follow the process of creating a vector from the thing you want to rotate to the thing you are rotating towards, projecting it onto a plane, and then, like with the canon rotation, and then get the angle between that vector and some initial reference vector that represents which way the object was facing before it started rotating.
You could also directly set the Euler angles of the rotation by using Quaternion.Euler, creating a variable to store the initial rotation value of the axis you want to rotate, and incrementing up and down depending on the angle between the object you want to rotate and the object you are rotating towards. BUT as Unity warns, the temptation to first read the Euler angles from a Quaternion, and then use those values to create a new rotation, will often result in unexpected and incorrect rotations.
Common Pitfalls and Useful Tips
Here’s a few pitfalls I found along the way that you would be wise to avoid:
- Everything is backwards - When creating or importing 3D models, sometimes what looks “forward” for the model, is actually “backwards” in Unity. Fortunately, creating vectors that point from an object to an object, you can just reverse the vector’s direction either by multiplying it by -1, or switching the order of your subtraction.
- Debug.DrawRay is your friend - Sometimes, for whatever reason, when creating vectors and projecting them, something has gone awry. The DrawRay function let’s you visualize vectors in Unity’s project window, and it has saved by behind on more than one occasion. Do I wish there was a Debug.DrawPlane? Yes. But I make do with what I’ve got ;)
No matter which way you choose to implement your rotation, projecting a 3D rotation or vector onto a 2D plane is an invaluable tool. Whether trying to get a single axis of a rotation, or just trying to wrap your head around a 3D rotation, the complexity of 3D space can always be reduced to something far more manageable and intuitive.
Feel free to use/modify this code, and the next time someone claims you are projecting, encourage them to do it to ;)