I have a situation where I’m dealing with just the matrices behind camera transformations, but not actual cameras. I already have working equivalents for the ViewportToWorld and WorldToViewport methods, as well as a camera projection matrix, a worldToLocal matrix, and a localToWorld matrix.

I’d like to have the functionality of the method ‘ViewportPointToRay’ in this class also, does anyone know how it might work behind the scenes?

Alternatively, if anyone knows how to find the world coordinates for where the corners of the viewport intersect with a plane at y = 0 using only matrices and not the ‘ViewportPointToRay’ method, that would also work!

For reference here is a gizmo I’m drawing just to show the frustum where y = 0. I basically want to not have to reference a camera in this method.

public void OnDrawGizmos()
{
Plane plane = new Plane(Vector3.up, Vector3.zero);
Vector3 bottomLeftViewport = new Vector3(0f, 0f, 0f);
Vector3 topLeftViewport = new Vector3(0f, 1f, 0f);
Vector3 topRightViewport = new Vector3(1f, 1f, 0f);
Vector3 bottomRightViewport = new Vector3(1f, 0f, 0f);
Ray ray;
float dist;
Vector3[] maxZoomFrustum = new Vector3[4];
ray = m_camera.ViewportPointToRay(bottomLeftViewport);
plane.Raycast(ray, out dist);
maxZoomFrustum[0] = ray.GetPoint(dist);
ray = m_camera.ViewportPointToRay(topLeftViewport);
plane.Raycast(ray, out dist);
maxZoomFrustum[1] = ray.GetPoint(dist);
ray = m_camera.ViewportPointToRay(topRightViewport);
plane.Raycast(ray, out dist);
maxZoomFrustum[2] = ray.GetPoint(dist);
ray = m_camera.ViewportPointToRay(bottomRightViewport);
plane.Raycast(ray, out dist);
maxZoomFrustum[3] = ray.GetPoint(dist);
Handles.color = Color.green.SetAlpha(0.3f);
Handles.DrawAAConvexPolygon(maxZoomFrustum);
}

ViewportToRay would work very similar to ViewportToWorld, but with the added step of finding the direction of said point from the camera;

Ray ViewportPointToRay (Vector3 pos, Camera cam)
{
//Remap to NDC-space [-1,1]
pos = pos * 2.0f - Vector3.one;
pos.z = 1f;
//Find the world-space position of the point at the camera's far plane
Vector3 worldPos = cam.cameraToWorldMatrix.MultiplyPoint (cam.projectionMatrix.inverse.MultiplyPoint (pos));
//The ray's origin is just the camera's position. Alternatively, you could use the same
//matrix logic above and find the same point at the camera's near plane and use that instead
Vector3 origin = cam.transform.position;
return new Ray (origin, worldPos - origin);
}

The Unity documentation for the internal function makes a very interesting point; the z-position is ignored. In this case, because distance is irrelevant in relation to a ray, we just assume the viewport point lies on the camera’s far plane.

I’ve just tested this one and it works quite well. The numerical error compared to Unity’s own ViewportPointToRay method is at the 5th decimal place. Drawing the rays 100 world units into the scene you can barely see the error. Unity might use some different internal order. However the results are pretty spot on.

public static Ray ViewportPointToRay(Vector2 aP, Matrix4x4 aProj, Matrix4x4 aCam)
{
var m = aProj * aCam;
var mInv = m.inverse;
// near clipping plane point
Vector4 p = new Vector4(aP.x*2-1, aP.y*2-1, -1, 1f);
var p0 = mInv * p;
p0 /= p0.w;
// far clipping plane point
p.z = 1;
var p1 = mInv * p;
p1 /= p1.w;
return new Ray(p0, (p1-p0).normalized);
}

Note that aProj need to be the camera’s projection matrix and aCam need to be the camera’s worldToCameraMatrix. Also note that worldToCameraMatrix is just the camera’s worldToLocal matrix but with the z axis inverted like this:

var w2c = Matrix4x4.Scale(new Vector3(1, 1, -1)) * cam.transform.worldToLocalMatrix;

I’ve tested this method with a perspective and an orthographic camera and it works like expected.