All matrix libraries for WebGL have some sort of perspective
function that you call to get the perspective matrix for the scene.
For example, the perspective
method within the mat4.js
file that's part of gl-matrix
is coded as such:
mat4.perspective = function (out, fovy, aspect, near, far) {
var f = 1.0 / Math.tan(fovy / 2),
nf = 1 / (near - far);
out[0] = f / aspect;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = f;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = (far + near) * nf;
out[11] = -1;
out[12] = 0;
out[13] = 0;
out[14] = (2 * far * near) * nf;
out[15] = 0;
return out;
};
I'm really trying to understand what all the math in this method is actually doing, but I'm tripping up on several points.
For starters, if we have a canvas as follows with an aspect ratio of 4:3, then the aspect
parameter of the method would in fact be 4 / 3
, correct?
I've also noticed that 45° seems like a common field of view. If that's the case, then the fovy
parameter would be π / 4
radians, correct?
With all that said, what is the f
variable in the method short for and what is the purpose of it?
I was trying to envision the actual scenario, and I imagined something like the following:
Thinking like this, I can understand why you divide fovy
by 2
and also why you take the tangent of that ratio, but why is the inverse of that stored in f
? Again, I'm having a lot of trouble understanding what f
really represents.
Next, I get the concept of near
and far
being the clipping points along the z-axis, so that's fine, but if I use the numbers in the picture above (i.e., π / 4
, 4 / 3
, 10
and 100
) and plug them into the perspective
method, then I end up with a matrix like the following:
Where f
is equal to:
So I'm left with the following questions:
- What is
f
? - What does the value assigned to
out[10]
(i.e.,110 / -90
) represent? - What does the
-1
assigned toout[11]
do? - What does the value assigned to
out[14]
(i.e.,2000 / -90
) represent?
Lastly, I should note that I have already read Gregg Tavares's explanation on the perspective matrix, but after all of that, I'm left with the same confusion.
Let's see if I can explain this, or maybe after reading this you can come up with a better way to explain it.
The first thing to realize is WebGL requires clipspace coordinates. They go -1 <-> +1 in x, y, and z. So, a perspective matrix is basically designed to take the space inside the frustum and convert it to clipspace.
If you look at this diagram
we know that tangent = opposite (y) over adjacent(z) so if we know z we can compute y that would be sitting at the edge of the frustum for a given fovY.
multiply both sides by -z
if we define
we get
note we haven't done a conversion from cameraspace to clipspace. All we've done is compute y at the edge of the field of view for a given z in cameraspace. The edge of the field of view is also the edge of clipspace. Since clipspace is just +1 to -1 we can just divide a cameraspace y by
-z / f
to get clipspace.Does that make sense? Look at the diagram again. Let's assume that the blue
z
was -5 and for some given field of viewy
came out to+2.34
. We need to convert+2.34
to +1 clipspace. The generic version of that isclipY = cameraY * f / -z
Looking at `makePerspective'
we can see that
f
in this casewhich is actually the same as
Why is it written this way? I'm guessing because if you had the first style and tan came out to 0 you'd divide by 0 your program would crash where is if you do it the this way there's no division so no chance for a divide by zero.
Seeing that
-1
is inmatrix[11]
spot means when we're all doneFor
clipX
we basically do the exact same calculation except scaled for the aspect ratio.Finally we have to convert cameraZ in the -zNear <-> -zFar range to clipZ in the -1 <-> + 1 range.
The standard perspective matrix does this with as reciprocal function so that z values close the the camera get more resolution than z values far from the camera. That formula is
Let's use
s
forsomething
andc
for constant.and solve for
s
andc
. In our case we knowSo, move the `c' to the other side
Multiply by -zXXX
Those 2 things now equal each other so
expand the quantities
simplify
move
zNear
to the rightmove
c * zFar
to the leftsimplify
divide by
(zNear - zFar)
solve for
s
simplify
change the
1
to(zNear - zFar)
simplify
simplify some more
dang I wish stackexchange supported math like their math site does :(
so back to the top. Our forumla was
And we know
s
andc
now.let's move the -z outside
we can change
/ (zNear - zFar)
to* 1 / (zNear - zFar)
soLooking back at
makeFrustum
we see it's going to end up makingLooking at the formula above that fits
I hope that made sense. Note: Most of this is just my re-writing of this article.
f
is a factor which scales the y-axis, such that all points along the top plane of your viewing frustum, post-perspective-division, have a y-coordinate of 1, and those on the bottom plane have a y-coordinate of -1. Try plugging in points along one of those planes (examples:0, 2.41, 1
,2, 7.24, 3
) and you can see why this happens: because it ends up with the pre-division y equal to the homogeneous w.