2

About 2 days ago I decided to write code to explicitly calculate the Model-View-Projection ("MVP") matrix to understand how it worked. Since then I've had nothing but trouble, seemingly because of the projection matrix I'm using.

Working with an iPhone display, I create a screen centered square described by these 4 corner vertices:

        const CGFloat cy = screenHeight/2.0f;
        const CGFloat z = -1.0f;
        const CGFloat dim = 50.0f;

        vxData[0] = cx-dim;
        vxData[1] = cy-dim;
        vxData[2] = z;
        vxData[3] = cx-dim;
        vxData[4] = cy+dim;
        vxData[5] = z;
        vxData[6] = cx+dim;
        vxData[7] = cy+dim;
        vxData[8] = z;
        vxData[9] = cx+dim;
        vxData[10] = cy-dim;
        vxData[11] = z;

Since I am using OGLES 2.0 I pass the MVP as a uniform to my vertex shader, then simply apply the transformation to the current vertex position:

uniform mat4 mvp;
attribute vec3 vpos;
void main()
{
  gl_Position = mvp * vec4(vpos, 1.0);
}

For now I have simplified my MVP to just be the P matrix. There are two projection matrices listed in the code shown below. The first is the standard perspective projection matrix, and the second is an explicit-value projection matrix I found online.

CGRect screenBounds = [[UIScreen mainScreen] bounds];
const CGFloat screenWidth = screenBounds.size.width;
const CGFloat screenHeight = screenBounds.size.height;

const GLfloat n = 0.01f;
const GLfloat f = 100.0f;
const GLfloat fov = 60.0f * 2.0f * M_PI / 360.0f;
const GLfloat a = screenWidth/screenHeight;
const GLfloat d = 1.0f / tanf(fov/2.0f);


// Standard perspective projection.
GLKMatrix4 projectionMx = GLKMatrix4Make(d/a, 0.0f, 0.0f, 0.0f,
                                         0.0f, d, 0.0f, 0.0f,
                                         0.0f, 0.0f, (n+f)/(n-f), -1.0f,
                                         0.0f, 0.0f, (2*n*f)/(n-f), 0.0f);
// The one I found online.
GLKMatrix4 projectionMx = GLKMatrix4Make(2.0f/screenWidth,0.0f,0.0f,0.0f,
                                         0.0f,2.0f/-screenHeight,0.0f,0.0f,
                                         0.0f,0.0f,1.0f,0.0f,
                                         -1.0f,1.0f,0.0f,1.0f);

When using the explicit value matrix, the square renders exactly as desired in the centre of the screen with correct dimension. When using the perspective projection matrix, nothing is displayed on-screen. I've done printouts of the position values generated for screen centre (screenWidth/2, screenHeight/2, 0) by the perspective projection matrix and they're enormous. The explicit value matrix correctly produces zero.

I think the explicit value matrix is an orthographic projection matrix - is that right? My frustration is that I can't work out why my perspective projection matrix fails to work.

I'd be tremendously grateful if someone could help me with this problem. Many thanks.

UPDATE For Christian Rau:

 #define Zn 0.0f
 #define Zf 100.0f
 #define PRIMITIVE_Z 1.0f

 //...

 CGRect screenBounds = [[UIScreen mainScreen] bounds];
 const CGFloat screenWidth = screenBounds.size.width;
 const CGFloat screenHeight = screenBounds.size.height;

 //...

 glUseProgram(program);

 //...

 glViewport(0.0f, 0.0f, screenBounds.size.width, screenBounds.size.height);

 //...

 const CGFloat cx = screenWidth/2.0f;
 const CGFloat cy = screenHeight/2.0f;
 const CGFloat z = PRIMITIVE_Z;
 const CGFloat dim = 50.0f;

 vxData[0] = cx-dim;
 vxData[1] = cy-dim;
 vxData[2] = z;
 vxData[3] = cx-dim;
 vxData[4] = cy+dim;
 vxData[5] = z;
 vxData[6] = cx+dim;
 vxData[7] = cy+dim;
 vxData[8] = z;
 vxData[9] = cx+dim;
 vxData[10] = cy-dim;
 vxData[11] = z;

 //...

 const GLfloat n = Zn;
 const GLfloat f = Zf;
 const GLfloat fov = 60.0f * 2.0f * M_PI / 360.0f;
 const GLfloat a = screenWidth/screenHeight;
 const GLfloat d = 1.0f / tanf(fov/2.0f);
 GLKMatrix4 projectionMx = GLKMatrix4Make(d/a, 0.0f, 0.0f, 0.0f,
                                          0.0f, d, 0.0f, 0.0f,
                                          0.0f, 0.0f, (n+f)/(n-f), -1.0f,
                                          0.0f, 0.0f, (2*n*f)/(n-f), 0.0f);

 //...

 // ** Here is the matrix you recommended, Christian:
 GLKMatrix4 ts = GLKMatrix4Make(2.0f/screenWidth, 0.0f, 0.0f, -1.0f,
                                0.0f, 2.0f/screenHeight, 0.0f, -1.0f,
                                0.0f, 0.0f, 1.0f, 0.0f,
                                0.0f, 0.0f, 0.0f, 1.0f);

 GLKMatrix4 mvp = GLKMatrix4Multiply(projectionMx, ts);

UPDATE 2

The new MVP code:

GLKMatrix4 ts = GLKMatrix4Make(2.0f/screenWidth, 0.0f, 0.0f, -1.0f,
                               0.0f, 2.0f/-screenHeight, 0.0f, 1.0f,
                               0.0f, 0.0f, 1.0f, 0.0f,
                               0.0f, 0.0f, 0.0f, 1.0f);

// Using Apple perspective, view matrix generators
// (I can solve bugs in my own implementation later..!)
GLKMatrix4 _p = GLKMatrix4MakePerspective(60.0f * 2.0f * M_PI / 360.0f,
                                          screenWidth / screenHeight,
                                          Zn, Zf);
GLKMatrix4 _mv = GLKMatrix4MakeLookAt(0.0f, 0.0f, 1.0f,
                                      0.0f, 0.0f, -1.0f,
                                      0.0f, 1.0f, 0.0f);
GLKMatrix4 _mvp = GLKMatrix4Multiply(_p, _mv);
GLKMatrix4 mvp = GLKMatrix4Multiply(_mvp, ts);

Still nothing visible at the screen centre, and the transformed x,y coordinates of the screen centre are not zero.

UPDATE 3

Using the transpose of ts instead in the above code works! But the square no longer appears square; it appears to now have aspect ratio screenHeight/screenWidth i.e. it has a longer dimension parallel to the (short) screen width, and a shorter dimension parallel to the (long) screen height.

I'd very much like to know (a) why the transpose is required and whether it is a valid fix, (b) how to correctly rectify the non-square dimension, and (c) how this additional matrix transpose(ts) that we use fits into the transformation chain of Viewport * Projection * View * Model * Point .

For (c): I understand what the matrix does, i.e. the explanation by Christian Rau as to how we transform to range [-1, 1]. But is it correct to include this additional work as a separate transformation matrix, or should some part of our MVP chain be doing this work instead?

Sincere thanks go to Christian Rau for his valuable contribution thus far.

UPDATE 4

My question about "how ts fits in" is silly isn't it - the whole point is the matrix is only needed because I'm choosing to use screen coordinates for my vertices; if I were to use coordinates in world space from the start then this work wouldn't be needed!

Thanks Christian for all your help, it's been invaluable :) Problem solved.

KomodoDave
  • 7,239
  • 10
  • 60
  • 92

1 Answers1

2

The reason for this is, that your first projection matrix doesn't account for the scaling and translation part of the transformation, whereas the second matrix does it.

So, since your modelview matrix is identity, the first projection matrix assumes the models' coordinates to ly somewhere in [-1,1], whereas the second matrix already contains the scaling and translation part (look at the screenWidth/Height values in there) and therefore assumes the coordinates to ly in [0,screenWidth] x [0,screenHeight].

So you have to right-multiply your projection matrix by a matrix that first scales [0,screenWidth] down to [0,2] and [0,screenHeight] down to [0,2] and then translates [0,2] into [-1,1] (using w for screenWidth and h for screenHeight):

[ 2/w   0     0   -1 ]
[ 0     2/h   0   -1 ]
[ 0     0     1    0 ]
[ 0     0     0    1 ]

which will result in the matrix

[ 2*d/h   0       0             -d/a        ]
[ 0       2*d/h   0             -d          ]
[ 0       0       (n+f)/(n-f)   2*n*f/(n-f) ]
[ 0       0       -1            0           ]

So you see that your second matrix corresponds to a fov of 90 degrees, an aspect ratio of 1:1 and a near-far range of [-1,1]. Additionally it also inverts the y-axis, so that the origin is in the upper-left, which results in the second row being negated:

[ 0   -2*d/h   0   d ]

But as an end comment, I suggest you to not configure the projection matrix to account for all this. Instead your projection matrix should look like the first one and you should let the modelview matrix manage any translation or scaling of your world. It is not by accident, that the transformation pipeline was seperated into modelview and projection matrix and you should keep this separation also when using shaders. You can of course still multiply both matrices together on the CPU and upload a single MVP matrix to the shader.

And in general you don't really use a screen-based coordinate system when working with a 3-dimensional world. You would only want to do this if you are drawing 2d graphics (like GUI elements or HUDs) and in this case you would use a more simple orthographic projection matrix, anyway, that is nothing more than the above mentioned scale-translate matrix without all the perspective complexity.

EDIT: To your 3rd update:

(a) The transpose is required because I guess your GLKMatrix4Make function accepts its parameters in column-major format and you put the matrix in row-wise.

(b) I made a little mistake. You should change the screenWidth in the ts matrix into screenHeight (or maybe the other way around, not sure). We actually need a uniform scale, because the aspect ratio is already taken care of by the projection matrix.

(c) It is not easy to classify this matrix into the usual MVP pipeline. This is because it is not really common. Let's look at the two common cases of rendering:

  1. 3D: When you have a 3-dimensional world it is not really common to define it's coordinates in screen-based units, because there is not et a mapping from 3d-scene to 2d-screen and using a coordinate system where units equal pixels just doesn't make sense. In this setup you most likely would classify it as part of the modelview matrix for transforming the world into another unit system. But in this case you would need real 3d transformations and not just such a half-baked 2d solution.

  2. 2D: When rendering a 2d-scene (like a GUI or a HUD or just some text), you sometimes really want a screen-based coordinate system. But in this case you most likely would use an orthographic projection (without any perspective). Such an orthographic matrix is actually nothing more than this ts matrix (with some additional scale-translate for z, based on the near-far range). So in this case the matrix belongs to, or actually is, the projection matrix. Just look at how the good old glOrtho function constructs its matrix and you'll see its nothing more than ts.

Christian Rau
  • 45,360
  • 10
  • 108
  • 185
  • Thank you so much for this very detailed response, Christian. I have two subsequent questions if you don't mind: – KomodoDave Oct 16 '11 at 20:17
  • FIRST: So if I (1) leave my perspective projection matrix as the matrix I first listed in my question, (2) include a view transform, (3) add user controls to scale and translate the view, and (4) specify `glViewport(0,0,screenWidth,screenHeight)` and `glDepthRangef(zNear,zFar)` then everything will be setup correctly as an MVP view? (As a side note: I understand that my "centered" square will not appear in the screen centre once I have done this.) – KomodoDave Oct 16 '11 at 20:19
  • SECOND: Actually no second question, just the FIRST one in the comment above, if you don't mind. – KomodoDave Oct 16 '11 at 20:24
  • @KomodoDave Don't use `glDepthRange`. The near-far range is specified in your projection matrix (like in your code sample), `glDepthRange` has a different purpose, you don't need it. You don't neccessarily need user controls to change the view. And with the matrix proposed above, your centered square should actually be centered (that's what the translation part will do). – Christian Rau Oct 16 '11 at 20:33
  • Ok Christian, message understood. I'll give this a go after dinner and leave another comment in about 30 mins to let you know how it's gone. – KomodoDave Oct 16 '11 at 20:46
  • I've added the matrix you suggested, but still nothing appears on screen. This is without a view matrix still (so only P and your new matrix) but shouldn't this work? Reverting to the static matrix works fine once again. I've added a new code listing at the bottom of my original post; it gives a summary of the key bits of code and the order they're in. Do you mind having a look and seeing whether there's something obviously wrong? – KomodoDave Oct 16 '11 at 21:40
  • @KomodoDave I hope the arguments to your `GLKMatrix4Make` function are column-major. – Christian Rau Oct 16 '11 at 21:50
  • Yes they definitely are. Man, I'm so fed up of this not working.. I expected it to take about 1 hour to implement and understand MVP and instead it's been virtually 2 whole days. What do you think I should do now? I'll have another look through the code and see if I can spot anything, but what I've listed above is all the fundamental function calls. – KomodoDave Oct 16 '11 at 21:55
  • I've added another update with some printout info. I've included my MV in the calculation now. The calculated values look correct, but I'm not seeing anything. I would assume that if I change my PRIMITIVE_Z (i.e. the depth of my vertices) or my view direction then the square would appear, but it hasn't so far... – KomodoDave Oct 16 '11 at 22:05
  • @KomodoDave a near value of 0 is not a good idea, just try 0.1 or something similar. And a z of 1 is neither a good idea, as now your primitves are actually at the location of the camera, try -1 or 0. – Christian Rau Oct 16 '11 at 22:18
  • Thanks Christian, I've adjusted the values. I still wasn't seeing anything though, and the printouts showed x,y values larger than 1. BUT I've transposed the matrix you gave me and now it works, although the square is no longer square. Does this make sense as a correct fix? The transform multiplication code is exactly as I've shown it in Update 2 in my original post; it hasn't changed, except for `ts` becoming its own transpose. – KomodoDave Oct 16 '11 at 22:52
  • Where does this additional matrix fit into the Viewport*Projection*View*Model*Position chain? I'm wondering how to correctly achieve what this matrix is intended to do; should I be adding a viewport transformation matrix and adjusting the view? The matrix would be perfect if only it didn't make my square non-square! – KomodoDave Oct 16 '11 at 23:19
  • I've added **UPDATE 3** to my original post just summarizing the progress, for your ease of reading. – KomodoDave Oct 16 '11 at 23:39
  • @KomodoDave Like said, the translate and scale into screen-based coordinates should not be done with a perspective projection, which you usually only use for a 3d scene. In this case a screen-based coordinate system doesn't make sense. For a 2d scene, where screen-based coordinates really make sense, you would use an orthographic projection. In this case the scale-translate matrix is the projection matrix (along with some scale and translate for z based on the near-far range). Just look how the [glOrtho](http://www.opengl.org/sdk/docs/man/xhtml/glOrtho.xml) function constructs its matrix. – Christian Rau Oct 17 '11 at 00:00
  • Ah of course, sorry, and thanks for the explanation. When using world coordinates for my vertices the square is perfectly square :) Thank you so much for all your help Christian, you've been a lifesaver! I can get on with the real work now, finally. – KomodoDave Oct 17 '11 at 00:18
  • @KomodoDave Ok, you got me to update my answer. For further question I would really advise you to look into some material on the basic transformation pipeline. Maybe the answers to [this question](http://stackoverflow.com/q/7377912/743214) about the fixed-function OpenGL transformation pipeline are of help to you, although many things you might already know. – Christian Rau Oct 17 '11 at 00:21
  • the funny thing is I've actually read a lot and understood a lot on how the render process works. What misled me early on was that I used `glDepthRange` when - as you pointed out - I shouldn't. This meant that some early tests I ran didn't work as they should have, and that made me think glViewport didn't do what I thought it did! So I then started thinking I had to apply my own Viewport transformation at the start, and that led me to this situation. It all seems very clear to me now, thanks to your explanation, and I'll be sure to back up this session with some repeated reading! – KomodoDave Oct 17 '11 at 00:54