Notifications
Article
Don't Forget Basic Math!
Updated 4 months ago
116
0
In this era of visual scripting and drag/drop development, sometimes the base fundamentals are lost and forgotten.
In this example I'll show a simple camera zoom script which functions in a similar way to some games I've seen released on Steam, as well as some improvements adding better functionality using a good old friend - trigonometry :)

Basic Example

public class Zoom : MonoBehaviour { Camera cam; float zoomMultiplier = 25; float zoomSpeed = 0.3f; float zoomVel; float targetZoom; float minZoom = 10f; float maxZoom = 60f; private void Awake() { cam = GetComponent<Camera>(); targetZoom = transform.position.y; } void LateUpdate() { float scroll = Input.GetAxisRaw("Mouse ScrollWheel") * zoomMultiplier; targetZoom = Mathf.Clamp(targetZoom - scroll, minZoom, maxZoom); Vector3 camPos = transform.position; float zoom = Mathf.SmoothDamp(camPos.y, targetZoom, ref zoomVel, zoomSpeed); camPos.y = zoom; transform.position = camPos; } }
Here we have a basic zoom function which smoothly raises the camera height using the SmoothDamp function. From watching the video, straight away something just 'feels' off about this zoom mechanic.
This feeling is caused by expecting the target point at the center of the view to be kept in frame while zooming and the camera having a view angle which is not directly looking down. Instead, the focus point is changing with the camera height, causing the player to then move the camera forward to get back to the point of interest. I have seen this numerous times, specifically in resource management sim games on Steam which tend to use a perspective camera looking at a downwards angle.

What We Need

Rather than raising and lowering the camera height directly, we need to move the camera along it's forward vector which will give the expecting feeling of zoom.
But exactly how much do we move it by? Using a little bit of trigonometry we can find out!
Let's flatten out our camera and draw some lines to see what we're dealing with... In diagram #1, we see the camera is pointing downwards at a 45 degree angle with it's height set to 5 units. If we draw some straight lines from the camera position to the ground, and the camera position along it's view vector - we get a triangle!
Using the above script for zooming, we are simply moving the camera in the Y axis only.
Diagram #2 now shows the camera's new height at 3, and in turn - a new look direction and target point. Clearly the new look target does not match the previous look target. In order to keep the target in frame, we need to adjust the new camera position in the X axis by the xDifference shown.
In diagram #3, if we slide xDifference up along the look direction vectors until it hits the edge formed by the camera height, we see it now forms a new triangle.
So - to keep our camera aligned with our view target, we need to shift it in the X axis by the value of xDifference. We know the angle of our camera and we know the length of HeightDiff. Calling on our knowledge from SOH-CAH-TOA - we can solve xDifference!
Since we have our adjacent edge (HeightDiff) and are solving our opposite edge from our angle (xDifference), we need help from TOA to solve - so our formula is....
xDifference = TAN(α)HeightDiff equals xDifference = TAN(45)2 equals xDifference = 1 * 2 equals xDifference = 2

Putting That Into Code

Now that we have a formula, let's put that into code!
void LateUpdate() { float scroll = Input.GetAxisRaw("Mouse ScrollWheel") * zoomMultiplier; targetZoom = Mathf.Clamp(targetZoom - scroll, minZoom, maxZoom); Vector3 camPos = transform.position; float zoom = Mathf.SmoothDamp(camPos.y, targetZoom, ref zoomVel, zoomSpeed); float heightDiff = camPos.y - zoom; float angle = (90 - transform.rotation.eulerAngles.x) * Mathf.Deg2Rad; float lengthDiff = Mathf.Tan(angle) * heightDiff; camPos.y = zoom; camPos.z += lengthDiff; transform.position = camPos; }
Calculating the correct angle to use will require subtracting the Euler angle from 90 and then converting it from degrees into radians since that is what Mathf.Tan will be expecting.
Hooray! Now the camera will zoom correctly keeping the target in frame!!!...... Kind of.... Until our camera's Y axis rotates. This formula doesn't account for us working in 3D space.
Not to worry! A quick matrix multiplication can get us out of this bind!
void LateUpdate() { float scroll = Input.GetAxisRaw("Mouse ScrollWheel") * zoomMultiplier; targetZoom = Mathf.Clamp(targetZoom - scroll, minZoom, maxZoom); Vector3 camPos = transform.position; float zoom = Mathf.SmoothDamp(camPos.y, targetZoom, ref zoomVel, zoomSpeed); float heightDiff = camPos.y - zoom; float angle = (90 - transform.rotation.eulerAngles.x) * Mathf.Deg2Rad; float lengthDiff = Mathf.Tan(angle) * heightDiff; camPos = transform.localToWorldMatrix.MultiplyPoint3x4(Vector3.forward * lengthDiff); camPos.y = zoom; transform.position = camPos; }
And here we have it! A zoom which correctly moves along the view vector using good old fashioned math! Note, the above change could also be written with camPos = transform.TransformPoint(Vector3.forward * lengthDiff); however I wanted to show what is actually happening.

Moving Forward

Using this knowledge, the same formula could be used to do something like make the camera zoom to where the mouse is pointing - see if you can work it out!
The point of this article wasn't so much "how to make a zoom script", rather, don't forget how valuable traditional methods and ways of thinking are.
Good luck with your developing!
Nathan Sheppard
Contractor - Programmer
8
Comments