Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Fit (Scale/Position) Camera Content inside RectTransform Area

Discussion in 'Scripting' started by jGate99, Oct 19, 2020.

  1. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,837
    Hi there,

    I have a 3D Cube and a Canvas UI (SCreen Overlay) with

    1- header
    2- Content area ( top anchored = 100, bottom anchored 200 points)
    3- Footer

    I want rescale/reposition (whatever the right way is) camera view in a way so it fits perfectly inside the rectransform content area.

    It requires math, something i'm really bad at, so please assist me


    Thanks


    Capture.PNG
     
  2. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    I can't implement this for you without getting involved too much, but I can help with stepping stones
    first of all (1), you need Camera.CalculateFrustumCorners

    check out the example to see how to use it.

    frustum is the cut-off pyramid shape that's cast by your camera (if you're in perspective mode).
    with this method, you get the four vectors (not points) that are assumed to originate from the camera position and go off precisely through the corners of your screen.

    with these vectors (imagine them as rays) it's now easy to compute where these rays will intersect with an imaginary plane that slices your cube in two halves (front and back).

    to do this, first let's assume that your cube's pivot is exactly at its center. this is your cube's position.
    now you can define a plane from two information available: A) any point that belongs to it, B) a normal vector

    this is the illustration for the normal vector
    upload_2020-10-19_1-33-26.png

    a point that belongs to it is your cube's position, and a normal vector in your case, is practically
    camera.transform.forward
    (or a negative of that to get an inverse), because you know that this plane is oriented exactly perpendicular to your eyesight.

    finally, when you have a plane made with a
    new Plane(...)
    , you can compute intersection by using the plane's Raycast method

    however, you now need to supply a Ray value to it. you can tell a ray if you know the following information: A) the origin of the ray, B) the direction of the ray.

    the vectors you got from Camera.CalculateFrustumCorners are your directions, and the origin is your camera position.

    now that you have the 4 intersecting points, let's just call them the frustum points, and let's move on to the second part.

    second of all (2), you have to make sure you have calculated how wide and tall your cube should be, in world units.
    to do this you need a bit of geometry. because your cube is rotated the way it is, you actually want to compute the extents of the 2D bounding box surrounding it.

    the easiest and the most versatile way to do this is to project all of its vertices back to your imaginary plane, and then you'll get some coordinates that lie in 2D space, from which you can easily obtain the bounds.

    projecting btw, means "finding some rays which pass through the cube vertices, then computing their intersection with the plane"

    for example, if you know that your cube is exactly 2 units wide/tall/deep, and its center is in the middle of it, then its eight vertices are distributed like this

    Code (csharp):
    1. // upper half
    2. 0: -1, 1, -1
    3. 1: -1, 1, 1
    4. 2: 1, 1, 1
    5. 3: 1, 1, -1
    6.  
    7. // lower half
    8. 4: -1, -1, -1
    9. 5: -1, -1, 1
    10. 6: 1, -1, 1
    11. 7: 1, -1, -1
    to make a ray for each vertex, you can find a vector between the vertex and camera position like this
    Code (csharp):
    1. Vector3 vec = camera.position - vertex[i];
    now this is a full span. to make a unit direction out of it, you need to normalize this.
    Code (csharp):
    1. Vector3 dir = (camera.position - vertex[i]).normalized;
    and now you can make a ray, because you know both its origin (the vertex) and its direction (this line above).
    apply Plane.Raycast like before to get the actual projection.

    (important edit: in fact, I was wrong here -- you cannot use the same imaginary plane as before because at least half of the points are on the wrong side of it; easy fix: make another plane that is the same thing as the previous one, but this one contains camera.transform.position instead of cube.transform.position; this will simply migrate this new plane to go through the camera instead, but is oriented the same; this way you're sure that all of the vertices lie in front of it, so no problems. important edit 2: sorry, nope :D this will invalidate the projection due to the fact that all vertex rays converge toward the camera position. you need to push the plane a little bit forward from the camera, and basically here is the distance you can use: camera.nearClipPlane; this is not a plane as suggested by the name, but merely a configurable distance, thus
    new Plane(-camera.transform.forward, camera.transform.position + camera.nearClipPlane * camera.transform.forward);
    ; this should work more or less, but there might be problems with accuracy)

    now you can create a Bounds object with your cube's center edit: camera's position as the first point, and set its size to
    Vector3.zero
    . you then use Encapsulate to add each projection point and this will grow the bounds automatically.

    keep in mind that Bounds is a 3D bounding box representation, so if your plane is angled in world space, the bounding box will be invalid, because it is always axis-aligned.

    if you know that your plane is always axis-aligned as well, then ignore this step, but if not, you can temporarily rotate your points to fall exactly onto the XY plane, for this step to work properly.

    to do this you need to find the world space rotation of your plane, and rotate the points mathematically before you add them to Bounds.

    for convenience, we'll pick the XY plane as our target plane, so the coordinates should all turn out as (x, y, n) where n will be the same for all projection points, and thus you can ignore it. this will effectively give you world-space independent 2D coordinates, from which you can easily obtain the screen-projected width/height of the cube.

    remember your plane normal vector? this is the direction in which your plane is oriented in the world space. you can access it through plane.normal

    the XY plane's normal is simply Vector3.back, it's a direction that points to you directly from the screen. to obtain the rotation from plane.normal to Vector3.back, you simply do

    Code (csharp):
    1. var rotation = Quaternion.FromToRotation(plane.normal, Vector3.back);
    now you simply multiply this with a projection point, and that's it
    Code (csharp):
    1. bounds.Encapsulate(rotation * projection[i]);
    (make sure to treat the cube's center edit: camera position point the same way when you're instantiating the Bounds object for the first time!)

    you should end up with a bounding box that has 3D extents where x will give you half of the cube's apparent width, y is half of the cube's apparent height, and z you can completely ignore (in fact, it should be very close to zero).

    finally (3), you can use all of this to come up with a basic proportion computation to change camera parameters (mainly you want to move it in the direction of -plane.normal, once you know its pointing to cube's center) in such a way that the 2D extents match up with the frustrum points.

    I understand this is too much text, but the actual code is really simple and without too many lines or some heavy math, so if you try to digest this step by step, it shouldn't be too hard for anyone really. if I'll have some spare time, I might do it, but in the meantime, try it on your own. no promises though.
     
    Last edited: Oct 19, 2020
    BlakkM9 likes this.
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    NOTE: I will use a letter Q to denote either width or height, but not both. You should pick the one that is greater, and this depends on your view aspect.

    To compute the camera distance, observe that there is a very simple linear relationship between A) distance between camera and cube's center, and B) ratio between extent Q and frustum Q.

    This helps you realize that there is a simple way to determine the distance (if the camera's field of view is constant), by taking the ratio and applying it directly to the current distance. If the extent Q is 2 times the frustum Q (the actual image of the cube is clipped, and you're too near), you need to move one full distance backward. Conversely, if the frustum Q is 3 times the extent Q (the cube is in the center of the view and too small, so you're too far away), you need to move to a third of a distance forward.

    Current distance is simply
    Code (csharp):
    1. float distance = (camera.transform.position - cube.transform.position).magnitude;
    or, the way beginners are accustomed to it
    Code (csharp):
    1. float distance = Vector3.Distance(camera.transform.position, cube.transform.position);
    Now you just need to find the adequate projection points in order to compute either width or height from them.
    You similarly compute distance for one pair of frustum points, and double the bounds.extents as they were (because they were already 2D; you can also use size for this), and then divide the results:
    extentDistance / frustumDistance
    .

    All in all
    Code (csharp):
    1. float ratio = extentDistance / frustumDistance;
    2. float distanceFromCube = (camera.transform.position - cube.transform.position).magnitude;
    3. float desiredDistance = ratio * distanceFromCube;
    4. camera.transform.position = cube.transform.position + desiredDistance * plane.normal;
    Now, I've probably messed up signs for normals, from my head, so this probably won't work out of box, but you should be fine if only you follow the logic through.
     
    Last edited: Oct 19, 2020
    jGate99 likes this.
  4. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    Oh for any of this to work as it should, you need to make sure that A) your cube's center is exactly in the middle, B) your camera looks directly at this point.

    Any deviations from this require the above math to be revised slightly, but it's not too much of a headache.
     
    jGate99 likes this.
  5. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,832
    Before you spend any time on that: Have you already considered how you want your game to react to devices with different screen aspect ratios? Just zooming in and out won't address that. And if you figure out a zooming formula that just assumes a single aspect ratio, you might need to throw it away in order to develop as plan for other aspect ratios.
     
  6. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    @Antistone that's true, and I've accounted for that in the solution above. He just needs to pick the greater of the two dimensions, in order to guarantee that the cube is always visible in its entirety.
     
  7. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,837
    Hi @orionsyndrome
    First of all, thank you very much for writing a detailed response for someone who should have studied math.

    Anyhow, coming to the point, i realized orthographic works best in my situation.

    And here is code that i googled and still i dont undrstand any math of it, it perfectly fits the cube inside the game view.
    All i need to is a way to position camera so cube appear perfectly centre BETWeen the content area (at green cube)

    Code (CSharp):
    1.  
    2. var size = Cube.renderer.bounds.size;
    3.  
    4. float screenRatio = Screen.width / (float) Screen.height;
    5.             float targetRatio = size.x / size.y;
    6.  
    7.             if (screenRatio >= targetRatio)
    8.             {
    9.                 return size.y / 2;
    10.             }
    11.          
    12.            
    13.            
    14.             float differenceInSize = targetRatio / screenRatio;
    15.  
    16. Camera.main.orthographicSize =  size.y / 2 * differenceInSize;

    Now with this code my Red Cube at 0,0,0 appears perfectly fit inside the gameview,
    now all i need is to position it in vertically centre of Content Area (space between header and footer)
    just like currently green cube is verticall centre inside content area.

    So if you could help me with code example, that'd solve my problem

    Thanks again

    Capture.PNG
     
  8. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    well, if this works for you, this means that your setup is axis-aligned, and this simplifies some things.

    the main difference between perspective and orthographic view, regarding camera frustum, is that orthographic has all rays going off in parallel. so it's not a cut-off pyramid anymore, but a cuboid.

    it comes with some conveniences however. namely, its width/height doesn't change with distance anymore, right?
    so this code can do Screen.width / Screen.height and run with it. this is why the perspective code is much more involved.

    there is also this orthographicSize property which is pretty straightforward and usable only in orthographic mode.

    also this line
    targetRatio = size.x / size.y

    basically "senses" your aspect ratio and adjusts parameters accordingly.

    what I can't tell from your image is how did you actually set up the cubes?
    is the green cube just a mockup? because if you look closely, your red cube is perfectly centered, both horizontally and vertically. if you want just to "skew" the camera so it offsets by some amount, I think you can use the oblique matrix projection (it's easier than it sounds).

    as per this article
    let's modify this code under "Setting frustum obliqueness using a script" so that you can configure it on the fly

    Code (csharp):
    1. using UnityEngine;
    2.  
    3. public class ObliqueCamera : MonoBehaviour {
    4.  
    5.   [SerializeField] Camera camera; // drag your camera here in the inspector
    6.   [SerializeField] float horizontal = 0f;
    7.   [SerializeField] float vertical = 0f;
    8.  
    9.   void Awake() {
    10.     if(camera == null) return;
    11.     var mtx = camera.projectionMatrix;
    12.     mtx[0, 2] = horizontal;
    13.     mtx[1, 2] = vertical;
    14.     camera.projectionMatrix = mtx;
    15.   }
    16.  
    17.   #if UNITY_EDITOR
    18.   void OnValidate() {
    19.     Awake();
    20.   }
    21.   #endif
    22.  
    23. }
    Attach this script anywhere, and link your camera to the eponymous field in the inspector. It should work in play-time only, and you should be able to change the two values (in play mode) and observe the results immediately.

    I hope this helps.
     
    Last edited: Oct 20, 2020
    jGate99 likes this.
  9. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    I haven't studied math btw (I mean not extensively, though I had more of it because of technical schools but I hated it). I had a barely passable degree in every school I attended. I am not even a math person. In fact, math is a lie. Sounds ridiculous, I know, but in general, it works completely inversely to what everyone is taught.

    People would say the same thing for music, if the only way to experience it was through the notation.
    Music notation is intimidating as hell!

    Same goes for math. Shake off that feeling of unworthiness, just a friendly word of advice from a fellow programmer, and do it from scratch. Problems like this really help to understand what's going on with it, why we do things the way we do them, and how to obtain the results. Theoretical math is something else, but the applied math is basically I have 5 apples and you stole 2, how many I've got left.

    Don't feel intimidated by it.

    (Now it's your turn to tell me the answer is 2 xD)
     
    Last edited: Oct 19, 2020
    jGate99 likes this.
  10. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,837

    Yes, Red is perfectly centre according to screen height

    BUT as im using canvas/ugui based screen overlay ui, and i want red to appear vertically centre in the space between header and footer (like green which i manually positioned to convey the end goal)

    now i thihnk this answer regarding rectransform sizes footer, header and content area (space between them) should solve the problem
    https://answers.unity.com/questions/1013011/convert-recttransform-rect-to-screen-space.html
     
  11. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    I really think you should try the last code, I think it suits you perfectly. It works independent of any UI, and because you have unequal heights of footer and header, gives you perfect control of the apparent view center, without changing the perceived camera projection. I've used this trick before in some projects, and it's awesome in moderation.
     
    jGate99 likes this.
  12. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    Ummm, I wrote OnAwake in the last codebox by mistake, fix that to Awake which is a valid name for the MonoBehaviour callback, otherwise it won't work at all. Silly.

    (fixed it in code for future reference)
     
    Last edited: Oct 20, 2020
  13. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,041
    @jGate99
    I stumbled upon this video I think you might find useful.
    basically a well-presented crash course on all essential math techniques for video games.

     
    jGate99 likes this.
  14. jGate99

    jGate99

    Joined:
    Oct 22, 2013
    Posts:
    1,837
    Thank you very much,
    This is a long road for me, so i decided to drop using camera etc and skip to ugui where doing is quite easy.
    i also found a camera fiit plugin on asset store and left the author a message and hopefully he have support for my rare case.