Austin Morlan


Code Contact LinkedIn



Sending Matrices to OpenGL

Introduction


Sending matrix data to OpenGL can be confusing, but it doesn't have to be.

The confusion arises due to misunderstanding two things: row vector / column vector and row-major / column-major.

Row Vector / Column Vector


Let's look at a 4x4 matrix (0-indexed to be programmer-friendly):

\( \begin{bmatrix} m_{00} & m_{01} & m_{02} & m_{03} \\ m_{10} & m_{11} & m_{12} & m_{13} \\ m_{20} & m_{21} & m_{22} & m_{23} \\ m_{30} & m_{31} & m_{32} & m_{33} \end{bmatrix} \)

There are two ways to multiply a vector by this matrix. You can multiply a row vector on the left:

\( \begin{bmatrix} x & y & z &w \end{bmatrix} \begin{bmatrix} m_{00} & m_{01} & m_{02} & m_{03} \\ m_{10} & m_{11} & m_{12} & m_{13} \\ m_{20} & m_{21} & m_{22} & m_{23} \\ m_{30} & m_{31} & m_{32} & m_{33} \end{bmatrix} \)

In which case the resultant vector is:

\( \begin{bmatrix} xm_{00} + ym_{10} + zm_{20} + wm_{30} & xm_{01} + ym_{11} + zm_{21} + wm_{31} & xm_{02} + ym_{12} + zm_{22} + wm_{32} & xm_{03} + ym_{13} + zm_{23} + wm_{33} \end{bmatrix} \)

Or you can multiply a column vector on the right:

\( \begin{bmatrix} m_{00} & m_{01} & m_{02} & m_{03} \\ m_{10} & m_{11} & m_{12} & m_{13} \\ m_{20} & m_{21} & m_{22} & m_{23} \\ m_{30} & m_{31} & m_{32} & m_{33} \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix} \)

And you get this:

\( \begin{bmatrix} xm_{00} + ym_{01} + zm_{02} + wm_{03} \\ xm_{10} + ym_{11} + zm_{12} + wm_{13} \\ xm_{20} + ym_{21} + zm_{22} + wm_{23} \\ xm_{30} + ym_{31} + zm_{32} + wm_{33} \end{bmatrix} \)

The resultant vectors differ in shape (column vs row), with the column being more compact visually, but more importantly the resultant values are very different.

So it's vital to know whether you're intended to multiply the matrix by row vectors on the left or column vectors on the right.

But how do you store a matrix or a vector in a computer?

Row-Order / Column-Order


There are two primary ways to define a 4x4 matrix in a language like C++:

// Sixteen consecutive floats
float matrix[16];

// 2D array
float matrix[4][4];

In both cases you have sixteen floats laid out consecutively in memory. The second method just makes it a little easier to index.

The computer can interpret those sixteen floats in two different ways. It can assume that it goes row-by-row, or it can assume that it goes column-by-column.

With row-by-row, or row-major, the computer would interpret the memory for the above matrix like this:

m00 m01 m02 m03 m10 m11 m12 m13 m20 m21 m22 m23 m30 m31 m32 m33

Column-by-column, or column-major, would instead be interpreted like this:

m00 m10 m20 m30 m01 m11 m21 m31 m02 m12 m22 m32 m03 m13 m23 m33

The actual matrix hasn't changed, it's just a matter of how those sixteen floats are stored in memory.

The same for a vector of four floats. The computer just stores four floats but it doesn't know whether they're laid out in a column or in a row.

OpenGL's Assumptions


When you send a matrix to OpenGL it assumes that the floats you send it are in column-major ordering, so that's how it will load the stream of floats into its own memory. If you stored them in your program in row-major, and then OpenGL interpreted them as column-major, your matrix would effectively be transposed on the OpenGL side, because it would read your matrix rows into its matrix columns.

\( A = \begin{bmatrix} m_{00} & m_{01} & m_{02} & m_{03} \\ m_{10} & m_{11} & m_{12} & m_{13} \\ m_{20} & m_{21} & m_{22} & m_{23} \\ m_{30} & m_{31} & m_{32} & m_{33} \end{bmatrix} \, \, A^{T} = \begin{bmatrix} m_{00} & m_{10} & m_{20} & m_{30} \\ m_{01} & m_{11} & m_{21} & m_{31} \\ m_{02} & m_{12} & m_{22} & m_{32} \\ m_{03} & m_{13} & m_{23} & m_{33} \end{bmatrix} \)

If you look back up above at the row-major and column-major float representations, you'll notice that a row-major ordering of \(A\) is the same as a column-major ordering of \(A^{T}\). And equivalently, a column-major ordering of \(A\) is the same as a row-major ordering of \(A^{T}\).

In other words, if OpenGL interprets your row-major floats as column-major, it's the same as if you had sent the transpose of your matrix instead.

What To Do?


I like to keep my matrices stored in row-major order and treat my vectors as column vectors. That way multiplying a matrix and a vector will access the matrix elements in a cache-friendly pattern (row-by-row).

A side benefit of using column vectors is that almost all linear algebra textbooks use column vectors, so it makes it easy to reference and use the textbook matrices without needing to transpose them.

Because I store them in row-major order and OpenGL interprets them as column-major, my matrices will effectively be transposed on the OpenGL side. There are two ways to counteract that.

Option 1) I can tell OpenGL to transpose my matrix before storing it (the third argument of glUniformMatrix4fv), which will put them back into the order that I expect. Then in the shader I can multiply the matrices by column vectors like I originally intended.

On the C++ side:

glUniformMatrix4fv(id, 1, GL_TRUE, &matrix[0][0]);

On the shader side:

gl_Position = projection * view * model * vec4(aPos, 1.0f);

Option 2) Or I can send my matrix to OpenGL as-is, and then in the shaders just assume that my vectors are row vectors instead.

On the C++ side:

glUniformMatrix4fv(id, 1, GL_FALSE, &matrix[0][0]);

On the shader side:

gl_Position = vec4(aPos, 1.0f) * model * view * projection;

The end result is identical. The product of the matrix \(A\) and a column vector is the same as the product of a row vector and the transposed matrix \(A^{T}\).

\( A \begin{bmatrix} x \\ y \\ z \\ w \end{bmatrix} = \begin{bmatrix} x & y & z & w \end{bmatrix} A^{T} \)

Example


As an example, let's send the following the model matrix to OpenGL for use in the vertex shader.

\( \begin{bmatrix} 5 & 0 & 0 & 2 \\ 0 & 5 & 0 & 2 \\ 0 & 0 & 5 & 2 \\ 0 & 0 & 0 & 1 \end{bmatrix} \)

We'll assume we're using column vectors, so we multiply a vector on the right, meaning that the vector will be uniformly scaled by 5 and translated in each direction by 2. The multiplication would look like this:

\( \begin{bmatrix} 5 & 0 & 0 & 2 \\ 0 & 5 & 0 & 2 \\ 0 & 0 & 5 & 2 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} 5x + 2 \\ 5y + 2 \\ 5z + 2 \\ 1 \end{bmatrix} \)

We'll store the matrix in row-major form, so the floats in memory will look like this:

5 0 0 2  0 5 0 2  0 0 5 2  0 0 0 1

If we tell OpenGL to transpose the matrix when it receives it, then the matrix on the OpenGL side will be stored as we expect, and in the vertex shader we can multiply as we would with column vectors:

gl_Position = model * vec4(aPos, 1.0f);

However, if we don't tell OpenGL to transpose the matrix, then it will be stored transposed on the OpenGL side, so in the vertex shader we can instead multiply as we would with row vectors:

gl_Position = vec4(aPos, 1.0f) * model;

Conclusion


The key takeaway here is that you only need to keep in mind two things: how are you storing your floats in memory on the application side, and how do you want OpenGL to store them on its side. If you store as column-major, then OpenGL will store them in the same way. If you store as row-major, then OpenGL will store them transposed. At that point you must multiply your vectors as either row vectors or column vectors, depending on your situation.