WaltN's picture

Surprising Result with ModelViewProjection Matrix

I'm definitely a newbie when it comes to new-age OpenGL and OpenTK, but I've got a few year's worth of experience with old-age OpenGL. This one surprised me: I set out to recode the SphereWorld example from SB5, Chapter 4, without using anything that was deprecated, but I couldn't get anything to render. Shaders, VBOs, VAOs were on my candidate suspect list because I had done little work in these areas. Transformations were there too because I learned a long time ago that, if you screwed up transformations, it could be that everything got clipped. I've mucked around with this for a few weeks, and this past weekend I decided to take a known good OpenTK example and to change one thing at a time until I got to where I wanted to go.

I started with OpenTK's HelloGL3 and backed out the normal data, changed it to render lines only, and introduced indices in order to use GL.DrawElements. I changed the shaders accordingly. It worked. That was easy. HelloGL3 sends to matrices to the vertex shader and concatenates them there. But it occurred to me that concatenating them in the shader might not be a good idea, since there was a chance that the matrix-matrix multiply might be executed once for every vertex (gl_Position = projection_matrix * modelview_matrix * vec4(in_position, 1);). I moved the concatenation back to the app using exactly the same projection and modelview matrices. I used the following code to implement the multiply:

        protected override void OnRenderFrame(FrameEventArgs e)
        {
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
 
            Matrix4.Mult(ref projectionMatrix, ref modelviewMatrix , out mvpMatrix);
            GL.UniformMatrix4(mvpMatrixLocation, false, ref mvpMatrix);
 
            GL.BindVertexArray(vaoHandle);
            GL.PolygonMode(MaterialFace.FrontAndBack, PolygonMode.Line);
            GL.DrawElements(BeginMode.Triangles, indicesVboData.Length,
                DrawElementsType.UnsignedInt, IntPtr.Zero);
 
            SwapBuffers();
        }

Before someone asks, here are the shaders:

        string vertexShaderSource = @"
#version 130
 
precision highp float;
 
uniform mat4 projection_matrix;
uniform mat4 modelview_matrix;
uniform mat4 mvp_matrix;
 
in vec3 in_position;
 
void main(void)
{
  //gl_Position = projection_matrix * modelview_matrix * vec4(in_position, 1);
  gl_Position = mvp_matrix * vec4(in_position, 1);
}";
 
        string fragmentShaderSource = @"
#version 130
 
precision highp float;
 
uniform vec4 color;
 
out vec4 out_frag_color;
 
void main(void)
{
  out_frag_color = color;
}";

Nothing rendered. I spent hours looking at the constituent matrices and consulting my library regarding transformations, an area I thought I was comfortable with. As a last gasp, I exchanged the positions of the ref arguments to Matrix4.Mult. It worked!

Just to see if the success was more than circumstantial, I went back to the SphereWorld example that I had abandoned and swapped the equivalent matrix arguments there. It worked there also.

Well, I've spent hours in the books and surfing the web to see if I could come up with a rationalization for this. But I'm here because that research effort was unsuccessful. My questions are:

  1. Can someone rationalize why P * M, where P is the projection matrix and M is the modelview matrix, works in the vertex shader while M * P works in the app?
  2. What are the performance implications of doing P * M in the vertex shader?

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
mOfl's picture

There is a difference between column-major and row-major storage of matrices in the memory which you can google. The main thing you have to know is that OpenGL uses the column-major convention, OpenTK offers row-major matrix structs. You can try a little 2x2 matrix multiplication on paper and you will see that A * B = (B'*A')' where ' means "transposed", i.e. swapping rows and columns of the matrix. You could also say column-major = (row-major)'. Therefore, the pre-multiplication of column-major matrices is equal to the post-multiplication of row-major matrices or in other words:

vertex * Model * View * Projection = (Projection' * View' * Model')' * vertex

The term on the left side is how the transformation of a vertex is usually written and the term on the right side is exactly what you need for the OpenGL shader. So in your C# code you do your row-major matrix multiplications:

Matrix4 modelView;
Matrix4 modelViewProjection;
Matrix4.Mult(ref model, ref view, out modelView);
Matrix4.Mult(ref modelView, ref projection, out modelViewProjection);

and then in the shader you transform your vertex:

vec4 transformedVertex = modelViewProjection * in_position;

You could also work with additional transpositions such that you can use the left side of the upper equation, but it would not change the result, but lead to more computation. The overhead of matrix multiplications in the vertex shader in general is pretty small because the graphics hardware is very good at floating point operations and you usually don't have that many vertices such that it would really impact performance (doing the transformation in the fragment shader, however, would be stupid ;)). However, it is always the best practice to do all uniform calculations once on the CPU as you thought yourself. But, unless you are doing high-performance applications, outsourcing the matrix multiplications to the CPU is rather a cosmetic change than a performance tweak.

WaltN's picture

OpenTK's row-major convention sure does explain it. I didn't know that; I just assumed it adopted the same convention as OpenGL. I guess the Uniform... wrapper functions must do a transpose to communicate matrices to the GPU.

You've done a lot to preserve my sanity. Thanks for a very thorough reply. And thanks for the perspective on the performance aspects.

mOfl's picture
WaltN wrote:

I guess the Uniform... wrapper functions must do a transpose to communicate matrices to the GPU.

Well, the transposition is done implicitly when uploading the matrix. The shader expects 16 float values in column-major format,

m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44

but what you upload with your OpenTK row-major matrix due to the arrangement in the memory is

m11, m21, m31, m41, m12, m22, m32, m42, m13, m23, m33, m43, m14, m24, m34, m44

which is already the transposed form of the upper one.

WaltN's picture

Neat! Thanks again for your elaboration.

Walt