Finally got it to work and it doesn't use sine, cosine, or arccosine so hopefully it is fast
There is a small problem that if VPN is the same as VUP it doesn't work. Not sure how to get around this condition but otherwise it works well. I'll explain it incase it is helpful for someone else in the future (I would be happy to hear other methods, this one just works for me).
VRC explanation:
I was thinking in terms of the VRC (Viewing-Reference Coordinate system) which is the diagram I posted above. VPN (View-Plane Normal) is the surface normal of the view plane (which I was calling the "virtual pixel grid" above). VRP (View Reference Point) is a point on the view plane that is needed for reference later. VUP (View Up Vector) is a required vector that is also needed for reference. The axes u, v, and n correspond to the equivalent of x, y, and z but relative to the VRC. Not in the diagram is the PRP (Projection Reference Point) which is where the "viewer" is.
I am assuming VRP is the center of the view plane but this is not always the case for VRCs in general. In implementation I would place VRP where the "object of attention" is. VPN is the direction vector to the "viewer" or PRP. The PRP is some distance along this vector where the distance is user specified (for zooming in / out).
Implementation:
Define VPN as a direction vector and VRP as a vector that is the point where the "object of attention" is. I set VUP as the y-axis (0, 1, 0) but this could be set otherwise.
Define uMin, uMax, vMin, and vMax as the width and height of the view plane from the VRP.
Calculate the u vector as the cross product of VUP and VPN (normalize vectors if necessary).
Calculate the v vector as the cross product of VPN and u (normalize if necessary).
Set a rotation matrix using u, v, and VPN like this:
u.x v.x vpn.x 0
u.y v.y vpn.y 0
u.z v.z vpn.z 0
0 0 0 1
Using a double for loop, create a grid of points centered on the xy-plane with z = 0 (or whatever plane you consider to be the "default" view) using uMin, uMax, vMin, and vMax. As Willvarfar mentioned, just corners could be calculated instead but I haven't implemented this myself yet. His suggestion should be faster. Multiply each point by the rotation matrix and then by a translation matrix that uses VRP's coordinates.
Hopefully this is helpful to someone