SOLVED. Problem was very simple, see the first reply. Thanks, randomphantom!
From what I understand, Core (and basically any UE4 game) use a fixed horizontal FoV - thats what we specify in the Camera settings. That all checks out, my math has confirmed that.
My ultimate goal is to create an anamorphic viewport, or at least emulate it by pinning things to the camera - but first I need to work out some initial math before I work on actual implementation, and sadly I can't even calculate the ideal camera distance to emulate vertical fov, despite it being highschool level trigonometry
Here's a simple walkthrough with a fresh blank project to easily reproduce my problem.
- Create a new Empty Project (no template)
- Delete "Third Person Camera Settings", add a "Top Down Camera Settings"
- Change rotation offset from -45 to -90 for a true top-down camera
- Drop into the Hierarchy a "Plane 1m - One Sided" - drop it in an empty space.
- Set scale of Plane to 10,10,1 and change the material to something visible (I chose 'Basic Material')
- Set Z position to 0.01 of the plane (so its visible in preview instead of fighting with the terrain).
At this point, I just start a non-MP preview to move the camera into place, then I press Esc - and I leave the preview with the current viewport in editor matching the starting game camera (it changes by a few pixels, meh):
Please take note of the Plane aligned to the grid. Specifically, how the camera at this window size can fit exactly one square of the terrain grid at the top and bottom of the 1m White Plane. Consider this our desired fixed viewport - eventually, anything that doesn't have any overlap with this square (when virtually "floored") will be culled.
But for now, I just need to try and get that square to "fill to fit screen" no matter the aspect. This means I want to fix my FoV on both sides, somehow. Can't do that directly with a perspective camera so we must emulate.
See for yourself. Start up a multiplayer preview window.
...ok, the aspect is already different from the Editor viewport - we can now see more on the top and bottom - it's 1.5 squares now instead of 1. I measured the size of the window content here for Bot1 (i.e. whole window excluding border/titlebar decoration) and its a tiny bit wider than 16:9, whatever.
Now, lets change it to something closer to 4:3...
So as you can see Core uses "Vert-" FoV, meaning the Horizontal FoV is fixed, but the vertical FoV is reduced as the screen widens (and is increased as the screen narrows). This means that in resolutions wider than a standard 16:9 (e.g. 21:9 is becoming more popular), the game port appears quite zoomed in:
I understand why this is good for FPS and such. I've read about why UE4 uses fixed horizontal FoV and I think its a good choice they made. But in a top down game, we don't want this simulation of peripheral vision that aligns with reality (i.e. the horizon). Especially is the case with a top-down game where you want the PvP arena to show the same world size to any resolution, even "portrait devices" wink wink. And of course this is one way (the only decent way I know of) to emulate a 2D game, or make some neat 2.5D ones.
So, its time to treat that square plane object as our viewport. Later on it might be a trigger that is pinned to the players feet. Don't worry about that part yet.
Right, here we go:
- Rename that 1m Plane to "_VIEWPORT"
- Make a new script, say "ViewportManager", and also make it a child of "Top Down Camera (client)".
local CAMERA = --[[---@type Camera]] script.parent --- properties (fixed) --- Opposite side of the triangle. Because we use a 1m square, we know 1m = 100 units, so its easy to get opposite from the viewport square local opposite = World.GetRootObject():FindChildByName("_VIEWPORT"):GetWorldScale().y * 100 / 2 --- horizontal fov local fovH = CAMERA.fieldOfView ---- vars (runtime) local screenSize = Vector2.New(-1, -1) -- a definitely not-sane default to force initial update function Tick(delta) --- PRIMARY REFERENCE: https://forum.unity.com/threads/camera-distance-based-on-object-size.762179/ --- SECONDARY REFERENCE: https://stackoverflow.com/questions/14614252/how-to-fit-camera-to-object --- SECONDARY REFERENCE [another one]: https://gamedev.net/forums/topic/686950-what-makes-a-vertical-fov-from-a-horizontal-one/5334971/ -- get the current screen size local newScreenSize = UI.GetScreenSize() -- only do stuff if screen size has changed if (newScreenSize.x ~= screenSize.x) or (newScreenSize.y ~= screenSize.y) then screenSize.x = newScreenSize.x screenSize.y = newScreenSize.y -- print new aspect ratio (as a ratio string) print("New aspect ratio > " .. newScreenSize.x / newScreenSize.y) -- first calculate the vertical FoV local fovV = 2 * math.atan(screenSize.y/screenSize.x * math.tan(math.rad(fovH)/2)) -- ...and convert to degrees then round up local fovVdegrees = math.deg(fovV) -- this seems absolutely correct. When going fullscreen in a 16:9 res, the fovV will be 59 (assuming fovH of 90) local fovVdegreesRounded = math.ceil(fovVdegrees) print("New fovV: " .. fovVdegreesRounded .. " degrees (rounded up from " .. fovVdegrees .. "; original rads = " .. fovV .. ")") -- now that we have foV, we can calculate cemera distance - adjacent - using THAT angle - in other words the hypotenuse is vertical now local angle = math.ceil(fovVdegrees / 2) local adjacent = opposite / math.tan(math.rad(angle)) print ("New distance: " .. adjacent) CAMERA.currentDistance = adjacent end end
This is ALMOST correct. It is very close to keeping the square locked to viewport, but it seems to be "snapping to nearest something". Or maybe the camera doesnt project a flat pyramid but a rounded cone or something like that that's beyond me.
Just try a multiplayer preview window again and make it nice and wide then narrow. I hope it makes sense when I say it seems like there's another variable in the Core viewport projection that i'm not accounting for.
What am I missing?
Thank you for reading!
~ [Minor edits to code and justifications/reasoning, all looks good now]