I’m currently working on a project where the camera is perspective (it needs to be in this case, “flattening” with orthographic won’t work as there are 3D components to the game!).
I have a GameObject which consists of three layers, and faces the camera (image 1). The problem I’m facing is that when I then move that GameObject up, the camera view-line no long hits each of those layers in the center, creating the perspective (image 2).
I’ve determined that the way to solve this is to offset each layer based on their distance from the camera such that the view-line always hits them in the center (image 3), essentially hiding the perspective.
Two questions:
Am I on the right track here in terms of this being the best way to accomplish this?
If so, I’m a little stumped on how I’d calculate the X and Y offset of each layer based on their distance from the camera and the GameObjects current position relative to it. Any pointers here?
The basic human field of vision is actually an oblique frustum: the point you are looking at (your gaze point) is much closer to the top edge of your field of vision than the bottom edge. This is likely because we are terrestrial animals and seeing the surface we are walking on is much more important than seeing the sky, on average.
You can look at 3D objects with an ortho camera all day long, from any angle. For an example, see the Monument Valley game.
Right I think I didn’t a bad job explaining here! I’m not sure it’ll work in my case because the objects that these layers are in need to be able to rotate and the depth needs to be visualized.
Are you perhaps looking for an oblique camera frustum?
I didn’t know about this and will definitely take a closer look, but my first hunch is that this won’t work because it’s possible that there are multiple objects (with layers) on the screen at at time, all of which are in different positions.
I could likely point the frustum at one of them to get the effect I’m looking for, but that would mess up the others I’m guessing?
Here’s a demo of what you can do with an oblique camera frustum:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// @kurtdekker - demoing an oblique camera frustum
//
// To use:
// - make a default scene with a Camera and Light
// - put this script on the Camera
// - press PLAY
//
// - use A/Z to move (dolly) in and out of screen
// - use arrows to adjust frustum obliquity
// - use HOME or 0 to return to non-oblique frustum
//
// Try using A/Z to dolly in and out of the screen.
// While doing this use arrows to adjust the frustum.
//
// Note how changing the obliquity keeps the axis of
// motion in and out of the screen the same.
//
// NOTE: it might LOOK you are rotating but look again:
// Your heading is NOT changing. The shape of your view
// frustum is distorting sideways.
public class DemoObliqueCameraFrustum : MonoBehaviour
{
Camera cam;
void Start()
{
cam = Camera.main;
// make a bunch of stuff ahead of us to observe
for (int i = 0; i < 100; i++)
{
Transform camTrans = cam.transform;
Vector3 pos = camTrans.position + camTrans.forward * 10;
pos += camTrans.right * Random.Range(-10.0f, +10.0f);
pos += camTrans.up * Random.Range(-10.0f, +10.0f);
pos += camTrans.forward * (i + Random.Range(0.0f, +20.0f));
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
cube.transform.position = pos;
}
}
const float changeSpeed = 0.25f;
const float inoutSpeed = 5.0f;
float horizObl;
float vertObl;
// see https://docs.unity3d.com/Manual/ObliqueFrustum.html
void SetObliquity()
{
Matrix4x4 mat = cam.projectionMatrix;
mat[0, 2] = horizObl;
mat[1, 2] = vertObl;
cam.projectionMatrix = mat;
}
void Update()
{
if (
Input.GetKeyDown(KeyCode.Home) ||
Input.GetKeyDown(KeyCode.Alpha0) ||
false)
{
horizObl = 0;
vertObl = 0;
}
float hmove = 0;
float vmove = 0;
if (Input.GetKey(KeyCode.LeftArrow))
{
hmove = -1;
}
if (Input.GetKey(KeyCode.RightArrow))
{
hmove = +1;
}
if (Input.GetKey(KeyCode.DownArrow))
{
vmove = -1;
}
if (Input.GetKey(KeyCode.UpArrow))
{
vmove = +1;
}
horizObl += hmove * changeSpeed * Time.deltaTime;
vertObl += vmove * changeSpeed * Time.deltaTime;
SetObliquity();
float zmove = 0;
if (Input.GetKey(KeyCode.A))
{
zmove = +1;
}
if (Input.GetKey(KeyCode.Z))
{
zmove = -1;
}
cam.transform.position += cam.transform.forward * (zmove * inoutSpeed * Time.deltaTime);
}
}
Ah, thanks for the demo Kurt, super helpful in explaining the concept! Unfortunately, I don’t think I’m going to be able to accomplish exactly what I’m looking for with it though as I need to be able to move the objects themselves, not just the camera / frustum.
I made a quick demo and took some screenshots to hopefully better illustrate the issue.
What I’m trying to accomplish is to have both the objects in the problem image to look like it does in the center-view. I know the easy answer to this is to just us an orthographic camera, which would definitely do what I want, but I want to be able to tilt these in various directions and still visualize the depth (hence the need for the perspective cam).
I know one solution here would be to set up two cameras, point them at the objects and output them to a RenderTexture. Then I can freely move the RenderTexture and have them stay consistent, though I was hoping to avoid as I’d need each layer to be masked separately (and in some cases tilt differently) so I’d need multiple RenderTexture setups (one per layer) and it could get quite complicated pretty quick…
So I was somewhat able to get this working by casting a ray from the camera through the center of the GameObject, and placing two planes on the Z-position of each of the front and back layers.
Ray ray = new Ray(Camera.main.transform.position, transform.position - Camera.main.transform.position);
RaycastHit hit;
float distance = 10f;
if (foregroundCollider.Raycast(ray, out hit, distance)) {
foreground.transform.position = hit.point;
}
if (backgroundCollider.Raycast(ray, out hit, distance)) {
background.transform.position = hit.point;
}
This works, but because the foreground layer is height-mapped to create a fake 3D effect from a 2D image, this creates some subtle sway in the foreground layer position (or at least the illusion of it). Still seeing if I can fix this, but thinking I may need to rely on the RenderTexture approach.
Check that you are updating these object positions via raycast explicitly AFTER you are updating the Camera’s position.
Easiest is to call one script from the other… the key is if those are in separate Update() scripts and the call orders change over time it can give the sensation of looseness as it moves one frame ahead, or one frame behind.
Confirmed that I’m definitely updating the position of the object after the camera position, it has to do with the height-mapped layer no longer matching the z-position that the plane is one, so it cannot position it correctly.
I think I could be thinking of this wrong, so I’ll zoom out a bit on the effect on trying to go for. I’m trying to create a trading card similar to the card effects using in Marvel Snap. As in, a card has multiple parallax layers, the top most layer is height mapped via a shader to create the illusion of 3D, and I can drag this card around the screen and not have the parallax layers get offset by the camera perspective (this is the part I’m struggling with)!
Here is a video from the gameplay demo which demonstrates a card being dragged around (see 8:39 timestamp), notice that the card tilts as if it’s 3D (leading me to believe the main camera is perspective), but no matter where the card is on the screen the card looks the same.