Think about a somewhat pseudo-isometric camera, like Diablo III--note that it’s not technically isometric in Diablo III, it’s just emulating an isometric projection; it’s just a plain old perspective camera locked in position at an angle above with respect to the scene. One of the weaknesses of such a camera is not being able to see ahead to understand where your enemies are. In a trigger-happy hack and slash game like Diablo III, this is fine. You’re not usually attempting to make fine-tuned tactical decisions. But in a game where you want to be able to plan ahead, seeing where enemies are situated before you run into them is key.
I apologize ahead of time for subjecting you all to my MSPaint “skills”.
|The eye in the upper left is the camera, and the square is the view: what you see on your monitor. The folks on the outside are the enemies.|
|If we just move the camera over (due to a fixed angle), we end up leaving some players out of the screen!|
Note I was told by our cinematics guy that this is in no uncertain terms called "zoom". Apparently zoom involves changing the field of view, and since we need that to be static, it's not zoom. He was quite insistent, and admittedly, he knows the terminology better than I.
|Rather, you want to pan to the center of them all, then pull back to ensure all actors you want are on screen.|
So what do we know? We know where the center of the camera is pointed at (our LookAt), we know where the camera itself is situated (our Camera), and we know/can calculate where the furthest actor we need to include in the view is (PointX).
Starting with these 3 points, we can actually calculate a pair of triangles to eventually be able to get to where we want our camera. Below depicts a pretty complex diagram of where we want to be, and all the pieces in-between we need to calculate. I'll break it down.
|Don't be fooled. I drew this as a right-triangle, but it's not guaranteed to be one.|
Note that we know the camera is fixed in field of view and frustum, and as such we know the top-most angle on both triangles will be identical (though we don't know the actual angle A). We also know that the bottom-left angle will be the same, and via the properties of a triangle, we know that all three angles must add to 180 degrees. The result? We're just trying to figure out how much to scale our triangle to fit the triangle to PointX.
If we can calculate the sides a, b, and d, we can figure out what e should be via simple ratios:
|Since we're just scaling, these ratios must be equal.|
We have the exact locations of three points, so we can calculate the side-lengths b and d trivially. a, on the other hand, may be a bit more difficult. We're missing what I've labelled PointP.
The thing about triangles is that if you have some combination of angles and sides, where you know at least one side, but a total of three things, you can calculate anything else you want about the triangle. However, all we have at the moment is side b. To get side a, we need more information.
We can calculate angle C. We can do so because we have two vectors: (Camera, LookAt), and (PointX, LookAt). How do you find the angle between two vectors? If your math library doesn't offer it, just make sure your vectors are at the origin, normalized, and the take the inverse cosine of the dot product. Unity thankfully does this for you via Vector3.Angle, though you'll still want to ensure they're at the origin.
But now we're at an impasse. We have one angle, and one side, but we can't calculate any more until we figure out what I've labeled PointP. Once we have PointP, getting side a is trivial, as it's just the distance between PointP and LookAt. We cannot calculate PointP with the information we have in the above diagram, however. We need another way to do it.
Let's go back to our full camera diagram. Note that PointP is on a straight line that passes through LookAt and PointX. Also note it passes through our camera frustum! If we can figure out the intersection of that line with the correct frustum plane, we can calculate the precise location of PointP in world space.
Frustum plane calculations are a bit intense, but thankfully Unity again comes to the rescue with GeometryUtility.CalculateFrustumPlanes. It returns all of the planes for our camera frustum. All six of them (near clipping, far clipping, and the four sides). Too many planes! How do we know which one is the correct one?
|Creating perspective camera diagrams in MSPaint is more time consuming than expected.|
That leaves us with four planes, and a vector (PointX, LookAt). To figure out which plane we're behind, we can do a quick angle check between each plane normal and our vector using the same technique we did earlier. Whichever angle is the smallest (we don't care about the sign) is the one we're behind.
|If you were to ask me to build a rigorous proof of this, I wouldn't be able to off-hand. But I'm fairly certain it works as long as LookAt is precisely in the middle of the frustum walls.|
|As a reminder of what we're deriving and what we have.|
Interestingly enough, this algorithm largely works in reverse, too, if all of your actors are inside the frustum and you need to push the camera back in. One of the vectors you use in a calculation above has to be reversed once you realize you're inside the frustum instead of outside, but you can re-use 95% of the same code. But I leave that as an exercise to the reader so I'm not giving ALL of our mathematical secrets away.
Note that this technique relies on the camera being static as far as field of view and rotation in the world goes. It should be fine to have different starting rotations, but once you've got the camera running, you're stuck (if you need to use fancy zoom in cameras for animation, etc. just duplicate the camera and move the duplicate instead). It also doesn't work if your near or far clipping planes are too close to the characters, but that generally isn't a problem in a Diablo III-style camera environment.
So there you have it. All of the math required to figure out how to fit all of your actors you need to on the screen at once, just to give folks a taste of what I do on a daily/weekly basis! I'm pretty lucky that I get to talk about things at this level of detail, and hopefully we'll have more meaty design-type stuff for folks eventually, too!