Normalized Device Coordinates

Hi guys,

Im having trouble getting my head around NDC. Assuming we are using OpenGL 3, and doing the perspective projection ourselves, do we have to worry about normalized device coordinates?

In perspective projection, a 3D point in a truncated pyramid frustum (eye coordinates) is mapped to a cube (NDC); the x-coordinate from [l, r] to [-1, 1], the y-coordinate from [b, t] to [-1, 1] and the z-coordinate from [n, f] to [-1, 1].

I am doing the perspective transform via a tutorial like this:


void perspective(GLfloat *matrix, GLfloat fov, GLfloat aspect, GLfloat nearz, GLfloat farz)
{
    GLfloat range;

    range = tan(fov * 0.00872664625) * nearz; /* 0.00872664625 = PI/360 */
    memset(matrix, 0, sizeof(GLfloat) * 16);
    matrix[0] = (2 * nearz) / ((range * aspect) - (-range * aspect));
    matrix[5] = (2 * nearz) / (2 * range);
    matrix[10] = -(farz + nearz) / (farz - nearz);
    matrix[11] = -1;
    matrix[14] = -(2 * farz * nearz) / (farz - nearz);
}

Kind regards,
Fugi

The glViewport call defines the screen device mapping after the perspective transformation.

The homogeneous numbers after the divide produce -1 to +1 x & y in screenspace and there is a transformation of this to pixels.

You do not have to worry about NDC except that you will want to make a glViweport call.

You will also want to send homogeneous undivided vec4 values out for all sorts of reasons, perspective correct zbuffer interpolation for example.

So is the divide that produces the NDC performed by glViewport or the perspective projection?

Neither. It’s sandwiched in between as a separate operation.

See the top diagram on this page.

Referring to the diagram:[ol][] Your 4x4 PROJECTION transform takes you from 4D eye coordinates to 4D clip coordinates.[] Then the perspective divide takes you from 4D clip coordinates to 3D NDC coordinates.[*] Then the viewport transformation takes those 3D NDC coordinates into 3D window coordinates.[/ol]

Ok, so the divide by w has to be performed by the perspective projection. The viewport transformation ‘expects’ the input to be in NDC otherwise it wont work.

In that case, does the following perspective transformation produces Normalized Device Coordinates?:


void perspective(GLfloat *matrix, GLfloat fov, GLfloat aspect, GLfloat nearz, GLfloat farz)
{
    GLfloat range;

    range = tan(fov * 0.00872664625) * nearz; /* 0.00872664625 = PI/360 */
    memset(matrix, 0, sizeof(GLfloat) * 16);
    matrix[0] = (2 * nearz) / ((range * aspect) - (-range * aspect));
    matrix[5] = (2 * nearz) / (2 * range);
    matrix[10] = -(farz + nearz) / (farz - nearz);
    matrix[11] = -1;
    matrix[14] = -(2 * farz * nearz) / (farz - nearz);
}


No.

The perspective projection is applied.
Then the divide by w is applied.
Then the viewport transform is applied.

The perspective projection application (matrix multiplication) doesn’t do the divide by w. It’s separate.

But yes, the viewport transform expects the perspective divide to have already been done, and thus accepts NDC as input.

In that case, does the following perspective transformation produces Normalized Device Coordinates?

No. Multiplying by a PROJECTION transform takes you to 4D CLIP space.

You have to then do the perspective divide to get to 3D NDC space.

EYE-SPACE --(PROJECTION TRANSFORM)–> CLIP-SPACE --(PERSPECTIVE DIVIDE)–> NDC-SPACE

Suggest you look back at this diagram and memorize it.

(And your matrix[0] term looks wrong. And you can simplify the perspective implementation a good bit. Check out Mesa3D’s gluPerspective implementation.)

Im a bit lost here, perhaps because of the difference between FFP and non-FFP way of doing things.

In FFP, I would set a viewport using glViewport and then the camera using gluPerspective, and not have to worry about the perspective divide. So, which function in the FFP does the divide? I didnt have to call a function to do so my guess is that its happening in the perspective matrix created by gluPerspective.

In my code, isn’t the line

matrix[11] = -1;

suppossed to do exactly that? Divide by -z to get clip coords?

According to the same songho website, its the perspective projection that maps to NDC(http://www.songho.ca/opengl/gl_projectionmatrix.html):

In perspective projection, a 3D point in a truncated pyramid frustum (eye coordinates) is mapped to a cube (NDC);

p.s: I got the perspective matrix from the OpenGL Wiki OpenGL 3.2 tutorials so its worrying that its wrong. I’ll have another look at it.

Simplification:

void perspective(GLfloat *matrix, GLfloat fov, GLfloat aspect, GLfloat nearz, GLfloat farz)
{
GLfloat top;

top = tan(fov * 0.00872664625) * nearz; /* 0.00872664625 = PI/360 */
memset(matrix, 0, sizeof(GLfloat) * 16);

matrix[0] = nearz / (top * aspect);
matrix[5] = nearz / top;
matrix[10] = -(farz + nearz) / (farz - nearz);
matrix[11] = -1;
matrix[14] = -(2 * farz * nearz) / (farz - nearz);

}

http://www.opengl.org/sdk/docs/man/xhtml/gluPerspective.xml

See, the whole thing is: the gpu does the division. You only need to feed it clipspace coords to gl_Position. If you’re providing gl_Position.w=1.0 , then the division doesn’t change XYZ, so you’re effectively giving NDC.

This “-1” in the matrix is just flipping the Z axis. The convention of GL is that Z- points forward in modelview space, but in viewspace the convention is that Z+ points forward. Yeah, a nasty tidbit imho, and I’m not sure why it’s like this. In your own code, be it FFP or non-FFP you are free to make all spaces point forward to Z+ ; (via glMultMatrix) that’s the first thing I tackled (I’ve seen many others here do this, too). Anyway, it’s nothing critical.

When you move from FFP to non-FFP, you simply copy and use the existing code for things like gluPerspective, it’s just that you manually create, multiply and upload matrices (that ultimately only calculate the ModelViewProjection matrix, which produces Clipspace coords).



glViewport(0,0,1280,720);
vec3 someVtx=vec3(5,6,7);
mat4 mPerspective = mat4::perspective(45.0f,...);
mat4 mView = mat4::lookat(...);
mat4 mModel = ...;

mat4 MVP = mPerspective*mView * mModel;

//--------[ GPU side !!! ]-------------------[
vec4 CLIPSPACE = MVP * vec4(someVtx,1.0f); // vertex shader!!
// this CLIPSPACE var is known as gl_Position


//------[ stuff out of your control ]-------[
vec3 NDC = vec3(CLIPSPACE.xyz/CLIPSPACE.w);

// well, you can tweak some of the numbers here, 
// i.e with glViewport()
float screenX = (NDC.x *0.5+0.5)*1280;
float screenY = (NDC.y *0.5+0.5)*720;
//------------------------------------------/
//-------------------------------------------/

1 Like

^Now that makes perfect sense! Thanks ilian, Dark Photon and Dorbie.