During the development of my new game currently called “Space Pew” I’ve come across a somewhat interesting problem.
I wanted to have the ships the player will control take a certain percentage of the screen. Essentially, when the longest axis of the ship was aligned with the smallest size of the screen, usually height, the first should be between 20% and 80% of the latter.
By example, say the screen has 1080 pixels in height, the ship should take no more than 864 pixels and no less than 216 pixels when its longest axis is aligned vertically.
I have spent hours looking for an already made solution to this and all I could find were expensive operations where you would project the bounding box’s corners on the screen space and then make your calculations all in screen space. And it baffeled me that that was all I could find since for me this was clearly, from the get-go, a conical projection problem.
I think the trickiest part here is to realize that the pixel size of the screen is completely irrelevant. All calculations are done in world space and really, all you need to know are just the following two things:
- Ship’s longest axis size, in game units
- Camera’s field of view setting
In Unity, the camera object exposes a “fieldOfView
” property. It is important to not that this is the vertical field of view of the camera. I was not expecting that when I started this but it works out in our favor.
Since we only worry about the size of the ship on a single axis and we are going to make our calculations for the worst case scenario (ship aligned along vertical axis of screen) we can reduce the problem, conceptually, from 3D to 2D.
Really, the question we’re asking is: “How far away from the object does the camera need to be so that at the current field of view setting my target object only takes between X and Y percentage of the screen’s height?”
The simple solution
The core of the issue is identifying that the resolution does not matter. We’re not interested in the pixel to unit ratio because that does not provide any additional information that we do not have already. Moreover, the scale between a pixel and a world unit is not constant at different angles due to the fisheye effect so we would not be able to use that anyway.
If you imagine a triangle, where the camera sits at the top and the top angle is the field of view setting that we got from it earlier on, then what we need to know is at what range bottom side is the length we need. This is somewhat straight forward to calculate and I will illustrate that with the help of the image below.
Let’s go over all of these points as the image is a bit busy.
Camera \rarr \text{Position of the camera} \\ \measuredangle{ACB} \rarr \text{Camera field of view angle} \\ |AB| \rarr \text{Total distance in units that fits the camera} \\ |A_1B_1| \rarr \text{Units along target longest axis} \\ MidPoint \rarr \text{The center of the AB segment} \\ |CM| \rarr \text{Distance from camera}
That should somewhat clear things up. Now, let’s see what we know and what we need to find out.
First, we know how many units our target object has along its longest axis. So we know the length of the A1B1 segment. Given that we know how much of the screen we want our object to take, as a ratio, we can calculate the AB segment. We also know the center of this segment as that is our target object’s position. The camera’s position is also known to us and as such we have the CM vector’s direction but not its magnitude yet. Given that we also know the angle ACB since it is the field of view setting on the camera, all that’s left to calculate is the length of the CM segment at which the AB segment covers the entire screen.
What we need to do is the following:
- Calculate the length of the AB segment given the length of the A1B1 segment and a ratio between the two
- Divide the length of the AB segment in half since we’ll be working with the ACM triangle only
- Divide the field of view angle setting value in half due to the same reason
- Given that we now know the length of the AM segment and the size of the ACM angle:
- Calculate the tangent of the ACM angle
- Divide the AM segment’s length by the tangent to get the CM segment’s length
- Place the camera that many units away from the target
If you want to explore this problem space a bit more, I’ve put together an interactive example that can show how the points move around. Keep in mind, it will only work as long as you change just the first three slider parameters. Since it is a simplified study case it will break if you try and move the camera. You can find it here: https://www.desmos.com/calculator/iyijvfncir
The code
Normally, I’d post a long file here full of code comments that explain the problem space but, since I’ve already done that I’ll only be covering some specifics around it.
Remember that “Camera.fieldOfView” returns the vertical angle of the camera’s field of view, not the horizontal one. This is very important as it is one of the core parameters of the entire solution.
When determining what to use as a guide for how much your object occupies on the screen you want to consider a worst case scenario when setting your ratio. By example, take the largest dimension of the target object and consider that against the smallest dimension of the screen, usually its height.
Making all these calculations is somewhat expensive in terms of processing power. If you need one number to set the camera and then leave it there, then you should consider calculating this just once, using the “Start” method to trigger your code.
If however you want to have a range of values that are allowed, say for example if you want the player to be able to zoom in and out whilst maintainging a minimum and maximum apparent size of the target object on the screen. Then it is best to calculate those two values at the start of the game and check if the zoom level desired is between them whenever the user makes a change.
In order to get the largest dimension of a target object you can use MeshFilter’s shared mesh bounds property. Since the object would be visible on the camera, it is a reaonable assumption to make that it has a MeshFilter component on it.
Limitations and considerations
After all that you should have a relatively clear understanding on how to aproach and solve the problem.
There are a few limitations to consider. If you want a highly irregular object to ocupy the same size on screen, regardless of orientation, then this is not for you. A more complex solution is required in those instances. I’m not sure what the usecase of that would be but that’s that.
If you want to use the screen’s horizontal size instead of the vertical one then you will need to figure out the horizontal field of view angle but the rest should still apply.
I hope this helps!