Skip to main content
GameDev.net gamedev.net
🔒 Locked 🎮 Unity

Perspective projection

Started by Gage64 Aug 30, 2008 at 2:57 AM 12 replies 1.7k views
Original Post
Gage64
Gage64
I'm having a hard time understanding perspective projection. I understand the basic idea and the derivation using right triangles, but it seems like that derivation assumes that: 1) The FOV is 90. 2) The aspect ratio is 1:1. What if I want to allow for other FOVs and aspect ratios? Also, the perspective transform transforms the frustum into a cuboid. How is this done? As I understand, one reason for doing this is to make 3D clipping trivial, but I also read somewhere that it "transforms" the perspective projection into an orthographic projection because it is more efficient and this can be exploited by the hardware. How does this work? Finally, it also places the near and far clipping planes at 0 and 1 or -1 and 1 (depending on the API). I think this is to help with Z-buffer precision, but I don't understand how this makes it any better. If anyone can explain even some of these things, or point me to some links, I would really appreciate it. Thanks in advance.
haegarr
haegarr
I don't know what "derivation using right triangles" means. But I'll try to shed some light onto the topic.

First, the perspective projection we are speaking about is a separable procedure w.r.t. the horizontal and vertical dimensions. That means that one can calculate with one of these dimensions while ignoring the other, and later can calculate with the other while ignoring the first. At last, both calculations are combined to yield in the result.

The only point of using right-angled triangles I see is to visualize the intercept theorem. In that case, the 90° angle is not located where the FOV is measured, but where the line of sight hits the near/far clipping planes (or view plane, if you want). Then, the line of sight and the half extent of the plane (in one of the separated dimensions) are the two legs of the triangle, and the hypotenuse is along the left or right or top or down (resp.) clipping plane. The FOV is then two times the angle between the line of sight and the said clipping plane (assuming we speak of a symmetrical view).

Now, assume that the view volume is a pyramid with the apex at the location of the eye/camera, and the mid-line along the line of sight, and the base being the far clipping plane. We want to scale that pyramid but not homogeneously but with a scaling factor varying along the line of sight. The scaling is oriented so that the space is downsized in horizontal and vertical dimension w.r.t. the view only. Moreover, we choose a scaling factor that minimizes the far plane to [-1,+1] in both dimensions. We want to do the same at the other end, but that isn't possible since the apex is a point only. So we first cut off the apex, making the pyramid to a frustum. So we yield in the near clipping plane that obviously must be apart from the apex (i.e. the near clipping distance must be > 0). Now we can scale the near plane also that it fits into the range [-1,+1] in both dimensions. All (virtual) parallel planes between the near and the far plane are scaled accordingly, what means that the scaling factor for a particular plane in-between can be computed by linear interpolation.

After that step the viewing frustum is transformed into a cuboid with a quadratic base. Scaling the depth dimension to [-1,+1] or [0,+1] is then no problem.

As you've already determined, clipping is now trivial, since clipping is now separated not only in the horizontal and vertical dimension but also in the depth dimension. I.e., in each dimension 2 simple comparisons are sufficient to decide whether or not to clip a primitive away.

You are also correct with the simpler drawing. All locations on rendered primitives that have the same horizontal and vertical dimension are now mapped onto the same pixel on the screen. Before the projection, the locations were also on a straight line, but the line has penetrated the view plane in a diversity of angles. I.e. the horizonal and vertical co-ordinates mapped to a particular pixel were dependend on depth.
Gage64
Gage64
Thank you for the detailed explanation.

I'll try to rephrase my first question.

I have a point (x, y, z) in 3D space and I want to project it onto a 2D plane to get (px, py). The plane is parallel to the XY plane, and it's distance from the origin is d, so the plane equation is z = d (I'm using a left-handed coordinate system as that's what I'm used to).

Using the right-triangle derivation I mentioned earlier, I get these equations:

px = (x * d) / z
py = (y * d) / z

As far as I know, I can choose d to be whatever I want, and the most convenient value for it would be 1, which would reduce the above equations to:

px = x / z
py = y / z

One of the things I forgot to ask about is, why is the value of d not important?

Now, APIs like D3D and OpenGL ask you to specify the vertical FOV and aspect ratio. How do they fit into the above equations?
Brother Bob
Brother Bob
Quote:
Original post by Gage64
Using the right-triangle derivation I mentioned earlier, I get these equations:

px = (x * d) / z
py = (y * d) / z

As far as I know, I can choose d to be whatever I want, and the most convenient value for it would be 1, which would reduce the above equations to:

px = x / z
py = y / z

One of the things I forgot to ask about is, why is the value of d not important?

It is not important where the on the near plane the projecton intersects the near plane. What matters is where, in relation to the viewport, they are projected. Try to add points representing the corners/edges of the viewport also and you will see that px and py always projects in the same relative spot within the viewport, independent of d.

Quote:
Original post by Gage64
Now, APIs like D3D and OpenGL ask you to specify the vertical FOV and aspect ratio. How do they fit into the above equations?

They don't fit into the equations above directly. What they do is change the shape of the viewport, which in turn controls the relative position of px and py within the viewport, which in turn affect perspective.

Increasing the horizontal FOV makes the viewport wider, thus making px project more towards the center of the viewport, therefore making you see more of the scene as more fits into the width of the viewport.

Aspect ratio is the same as FOV in that sense, but FOV is more of an absolute measure of the width and height of the viewport, but aspect ratio is the relative size of the width and height.
Gage64
Gage64
Quote:
Original post by Brother Bob
It is not important where the on the near plane the projecton intersects the near plane. What matters is where, in relation to the viewport, they are projected. Try to add points representing the corners/edges of the viewport also and you will see that px and py always projects in the same relative spot within the viewport, independent of d.


I don't understand how the viewport fits into this. If I'm understanding what you're saying correctly then, in more concrete terms, if I increase or decrease d, it will cancel out somewhere down the line. Are you saying that will happen during viewport mapping? How?

What is the connection between the viewport and d?
Brother Bob
Brother Bob
I will take an example on the X-axis only. Same applies to Y of course.

Let's say you have a FOV of 90 degrees and a near clip plane distance of 1. Simple values. The left and right viewport edges intersects the near plane at x=-1 and x=1. Now project a point (x,z)=(1,2) (omitting Y-coordinate, since we focus on X-axis only) onto the near clip plane, and it will end up at xn=0.5 on the near clip plane.

Inside the viewport, this corresponds to normalized device coordinates as 0.25, becuase it's 0.25 from the left edge to the right edge of the viewport, where 0 is the left and 1 is the right edge.

Now project on another near clip plane value instead; say 0.1. The viewport edges now end up at x=-0.1 and x=0.1. The point (x,z)=(1,2) projects to xn=0.05. See how, in normalized device coordinates, the point is still at 0.25? No matter what distance you have to the projection plane, the point will always project to 0.25 in normalized device coordinates.

Normalized device coordinates is then subject to the viewport transform to transform the logical coordinates into physical pixel coordinates, but of normalized device coordinates are the same, then so are the pixel coordinates.

So the cancellation is basically when you turn the projected coordinates into normalized device coordinates. This is becuase, when you scale the near plane, you scale the resulting coordinates, but you also scale the corresponding viewport.
haegarr
haegarr
If you have a view parallel plane at distance D and with the extent (W,H), then the aspect ratio is
r := W/H
From the correspondence
tangens = length-of-opposite-leg / length-of-adjacent-leg
in a right-angled triangle, you get
tan( fh/2 ) = W / ( 2D )
tan( fv/2 ) = H / ( 2D )
where fh and fv are the horizontal and vertical FOV, resp. You can substitute the one or other if using the aspect ratio, since
tan( fh/2 ) / tan( fv/2 ) = W / ( 2D ) / ( H / ( 2D ) ) == r
Hence, with e.g. the vertical FOV and the aspect ratio you have a full definition of the horizontal and vertical dimensions of the view frustum. Alternatively, due to the above equations, one can use the extent of a plane together with its distance to yield in the same frustum. E.g. OpenGL's glFrustum uses the near clipping plane for this purpose: left, right, bottom, and top denote its extent, and zNear its distance, of course.
Gage64
Gage64
Quote:
Original post by Brother Bob
Inside the viewport, this corresponds to normalized device coordinates as 0.25, becuase it's 0.25 from the left edge to the right edge of the viewport, where 0 is the left and 1 is the right edge.


"normalized device coordinates" is another term I wanted to ask about. Could you please explain how you got 0.25? (I understand how you got the 0.5 before that, BTW)

Also, just to clarify things for myself, I think we might be using the term "viewport" to mean different things. From your description above, I understand it as "the area of the projection plane that is inside the view frustum", while I used it to mean "the area on the screen that the final image gets rendered to".

With the first meaning, the viewport is specified by the frustum's horizontal and vertical FOVs and the distance to the near clipping plane. With the second meaning, it is completely specified by the user (e.g., (20, 20, 320, 240)).

Did I at least get this part right? [smile]

And finally, in your example the projection plane and the near clipping plane are the same. Is this required?

BTW, if at this point you are getting tired of my endless stream of questions, I understand. Tomorrow I'll have access to some computer graphics books and I'll try to read more about this, as apparently my understanding of this is even worse than I thought...
Zipster
Zipster
First, re-write the top two equations as px = x * (d / z) and py = y * (d / z) (basically re-associating the values). What these equations tell us is that we're finding the ratio between the projection plane distance and the point's Z value, and multiplying that by the X value. This will essentially "pull" the point towards the projection plane and tell us what the X value should be when the point is projected onto the plane.

The problem is that 'px' and 'py' haven't been normalized yet, just projected onto the plane. This projection plane is being cut on four sides to form the viewport (floating in 3D space, as it were). This viewport (which you can probably think of as a floating quad at this point) has a certain size. We know it is 'd' units from the origin. Given a vertical FOV, we can calculate the half-height of the viewport: d * tan(FOV / 2). We only need half the height (and hence half the vertical FOV) because it's symmetrical. Anything we do in +Y also works in -Y. The half-width is thus d * tan(FOV * aspect_ratio / 2). The aspect ratio is how many times larger the horizontal FOV is than the vertical FOV. Think of it in terms of a television's FOV -- 4:3, 16:9, etc. To normalize 'px' and 'py', we just divide by those quantities. However notice what happens when we divide both sides of the equation. We end up with nx = (x / z) * (1 / tan(FOV * aspect_ratio / 2)) and ny = (y / z) * (1 / tan(FOV / 2)). That's why 'd' isn't important (it just cancels out), how you incorporate aspect ratio, and why you'll find 1 / tan() = cot() lurking around in a perspective matrix ;)

There's also another way to think about this that uses the properties of similar triangles. Start with a point [x,y,z] and a vertical FOV. What is the 'z' value along the edge of the frustum associated with the given 'y'? y / tan(FOV / 2). Essentially, we just "moved" the point along the Z-axis until we hit one of the side planes of the frustum, and grabbed that 'z' value. But what is the actual 'z' value of this point? Well it's just the 'z' we were given. Using similar triangles, the ratio between the projected 'z' and the real 'z' should be the same as the ratio between the projected 'y' and the real 'y'. This ratio is of course what we found earlier, (y / z) * (1 / tan(FOV / 2)). Only this time we thought purely in terms of Z and similar triangles.

[Edited by - Zipster on August 30, 2008 6:23:29 AM]
haegarr
haegarr
Well, I'm not 100% sure about the following. So Brother Bob or someone else may correct me.

As Brother Bob has shown above, there is no need for a special view plane in the sense of the target of the projection.

However, the d in
x' := ( x * d ) / z
y' := ( y * d ) / z
is IMHO neither arbitrary nor senseless. In fact, it gives a constant scale on the depth fore-shortening, since after a little re-arrangement
x' = x / ( z / d )
y' = y / ( z / d )

A plane in distance d can hence be interpreted as a "special view plane" in that the depth fore-shortening just cancels out for any point [ x y d ] on that plane, i.e.
x' = x / ( d / d ) == x
y' = y / ( d / d ) == y
This is a useful feature for true-to-scale projections (well, I know no better word for what I mean here), e.g. in architecture or object design.
Mike nl
Mike nl
Lots of other answers given since I started typing this, but I'll be damned if all that work was for nothing. So at the risk of repeating previous answers, here goes. This may repeat stuff you already know as well, but I like to give complete answers.

First an image to show what I'm talking about:



Now, by the triangle relations, we get that

px / d = x / z, or
px = x * d / z

just like you said. However, you pick a random value for d. But let's look at this in detail. We want a point on the projection plane (z = d), at the edge of our view to have a value of 1. If we define αx = FOVx / 2 (we only care about half the FOV for the triangle math), then

tan(αx) * d = 1
d = 1 / tan(αx)
d = cot(αx)

Plug this into the first result:

px = x * cot(αx) / z

And there you go. Similary for y:

py = y * cot(αy) / z

Now, we want this expressed as a matrix, and those division by z in px and py can't be done normally, so we use homogenous coordinates, which means we can rewrite it to:

px = x * cot(αx) / w
py = y * cot(αy) / w

And make w = z

To get Z, we must realise that we want to map d to 0 and f to 1. And since Z doesn't depend on X and Y, Z is the result of a linear combination:

pz = z * p + q

But, it's also divided by w (which is z), so that becomes:

pz = (z * p + q) / z

Since we know that d becomes 0 and f becomes 1, we can solve p and q:

pz * z = z * p + q

For z = d and pz = 0:
0 * d = d * p + q
q = -d * p

For z = f and pz = 1:
1 * f = f * p + q
f = f * p + -d * p
f = p * (f - d)
p = f / (f - d)

Plugging p into q:
q = -d * p
q = -d * f / (f - d)

So,
pz = (z * f / (f - d) + -d * f / (f - d) ) / z

Combining this all into a (DirectX style) matrix:
cot(FOV<sub>x</sub>/2)         0             0           0     0        cot(FOV<sub>y</sub>/2)         0           0     0             0            f / (f - d)   1     0             0       -d * f / (f - d)   0

Which is exactly what DirectX and OpenGL produce for a FoV-based perspective projection matrix. The relation between FOVx and FOVy is of course:
FOVx / FOVy = aspect ratio = width / height
tan(FOVx) / tan(FOVy) = aspect ratio = width / height (Edit: quite right, haegarr)

So this projection maps everything in the view frustum to the cube (-1,-1,0) - (1,1,1). And (-1,-1) - (1,1) gets mapped onto (0,0) - (width,height) of the window/viewport/device/whatever.

[Edited by - Mike nl on August 30, 2008 7:51:20 AM]
Million-to-one chances occur nine times out of ten!
haegarr
haegarr
Quote:
Original post by Mike nl
Lots of other answers given since I started typing this, but I'll be damned if all that work was for nothing.

And you're right: Already the image gives this thread a fresh touch :)

But in
Quote:
Original post by Mike nl
FOVx / FOVy = aspect ratio = width / height

I'm missing some tangens.
Brother Bob
Brother Bob
Quote:
Original post by Gage64
Quote:
Original post by Brother Bob
Inside the viewport, this corresponds to normalized device coordinates as 0.25, becuase it's 0.25 from the left edge to the right edge of the viewport, where 0 is the left and 1 is the right edge.


"normalized device coordinates" is another term I wanted to ask about. Could you please explain how you got 0.25? (I understand how you got the 0.5 before that, BTW)

Normalized device coordinates is just a coordinate system that is resolution independent and normalized. Basically, 0 is the left edge, and 1 is the right edge (0 and 1 for bottom and top also). No matter what the actual resolution is, what aspect ratio is used, what FOV is used, this coordinate system is the same. Basically the last step in converting into window coordinates.

It's all about "reverse interpolation". If you have two values A and B, and a value C in between the two, what is the fraction of the distance between A and B that C is located at?
A----C---------B

Here, C is located at 0.3333... between A and B. A and B corresponds to the viewport edges, and C is the projected point. In Mike nl's picture, A=-1, B=1 (A=-B between becuase of symmetry in the viewport, but this is not necessary in general) and C=Px.

Quote:
Original post by Gage64
Also, just to clarify things for myself, I think we might be using the term "viewport" to mean different things. From your description above, I understand it as "the area of the projection plane that is inside the view frustum", while I used it to mean "the area on the screen that the final image gets rendered to".

With the first meaning, the viewport is specified by the frustum's horizontal and vertical FOVs and the distance to the near clipping plane. With the second meaning, it is completely specified by the user (e.g., (20, 20, 320, 240)).

Did I at least get this part right? [smile]

Two definitions of viewport meaning the same thing, really. Both defines an area of interest that you look at your whole scene through. Whether it's in eye space or window space is just a linear one-to-one transformation of difference.

Quote:
Original post by Gage64
And finally, in your example the projection plane and the near clipping plane are the same. Is this required?

As the examples above have shown, it doesn't matter where the projection plane is located at, and so it doesn't matter if the projection plane and near plane are the same or not. What the near plane affects, on the other hand, is depth value precision distribution. Standard link for Z-buffer precision and the related issues.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.