cglm
cglm copied to clipboard
Non-square matrix * column vector multiplication implemented as row vector * matrix multiplication
The matrix-vector multiplication functions for non-square types (mat4x3, mat3x4, mat2x4, etc) incorrectly take the dot product of each column of the matrix with the vector instead of the row, which is vector * matrix not matrix * vector. This also means the functions take and output the wrong dimension of vector. For example, the mat4x3 multiplication:
A mat4x3 is a matrix of 4 columns and three rows:
| m00 m10 m20 m30 |
| m01 m11 m21 m31 |
| m02 m12 m22 m32 |
A valid matrix-vector multiply on this matrix would take a vec4 and output a vec3 where the first component of the vec3 is the dot product of the first row of the matrix with the vector:
d0 = (m00 * v0) + (m10 * v1) + (m20 * v2) + (m30 * v3)
instead, the glm_mat4x3_mulv takes a vec3 and outputs a vec4, where the first component of the output vec4 is
d0 = (m00 * v0) + (m01 * v1) + (m02 * v2)
The square matrix types correctly dot the rows of the matrix with the vector to get each component of the output: https://github.com/recp/cglm/blob/45134b126548206b669154fb4c62ab8c0fc81206/include/cglm/mat4.h#L407
@Error-mdl
Seems like the main issue here is perspective of what the matrix looks like under the hood.
@recp We should probably document this. What do you think?
You say https://github.com/recp/cglm/blob/45134b126548206b669154fb4c62ab8c0fc81206/include/cglm/mat4x3.h#L151
A valid matrix-vector multiply on this matrix would take a
vec4 and output a vec3
where the first component of the vec3 is the dot
product of the first row of the matrix with the vector:
Per my understanding of linear algebra
You can't multiply
4x3 by a 4x1 # Due to GLSL Spec <column X row>
But you can multiply
4x3 by a 3x1 with the result being 4x1
I decided when creating arrays to use
column left, row right (top of bellow table)
versus
column right (top of bellow table), row left
So,
| m00 m10 m20 m30 |
| m01 m11 m21 m31 |
| m02 m12 m22 m32 |
(column top, row left) Gives you.
| column 1 | column 2 | column 3 | column 4 | |
|---|---|---|---|---|
| row 1 | m00 | m10 | m20 | m30 |
| row 2 | m01 | m11 | m21 | m31 |
| row 3 | m02 | m12 | m22 | m32 |
From a programming perspective, if you typed as is
float v0 = v[0], v1 = v[1], v2 = v[2];
// [column][row]
dest[0] = m[0][0] * v0 + m[1][0] * v1 + m[2][0] * v2 + m[3][0] * Doesn't exist;
dest[1] = m[0][1] * v0 + m[1][1] * v1 + m[2][1] * v2 + m[3][1] * Doesn't exist;
dest[2] = m[0][2] * v0 + m[1][2] * v1 + m[2][2] * v2 + m[3][2] * Doesn't exist;
dest[3] = Missing data
If the row was on top then using a vec4 (4x1) would be valid.
However, that changes the matrix to 3x4 multiply by 4x1
(column left, row top) Gives you
| row 1 | row 2 | row 3 | |
|---|---|---|---|
| column 1 | m00 | m01 | m02 |
| column 2 | m10 | m11 | m12 |
| column 3 | m20 | m21 | m22 |
| column 4 | m30 | m31 | m32 |
From a programming perspective, if you typed as is
float v0 = v[0], v1 = v[1], v2 = v[2];
// [column][row]
dest[0] = m[0][0] * v0 + m[0][1] * v1 + m[0][2] * v2;
dest[1] = m[1][0] * v0 + m[1][1] * v1 + m[1][2] * v2;
dest[2] = m[2][0] * v0 + m[2][1] * v1 + m[2][2] * v2;
dest[3] = m[3][0] * v0 + m[3][1] * v1 + m[3][2] * v2;
For Reference Sake
https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.pdf
From the openGL 4.60 spec page 126:
This clearly states the product of a vector V and a matrix M is the vector of the dot product of V with the columns of M, while the product of M with V is a vector of the dot products of the rows of M with V.
Currently, the 4x4, 3x3, and 2x2 matrix-vector multiply functions are all implemented as returning the vector of the rows dotted with the input vector. The non-square types return the vector of the columns dotted with the input vector. This is inconsistent, and one has to be wrong.
My 4x3 matrix example is trivially verifiable with a simple glsl shader:
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
// matrix of 4 columns and 3 rows
mat4x3 matrix = mat4x3(
vec3(0, 0, 0),
vec3(0, 0, 0),
vec3(0, 0, 0),
vec3(1, 0, 1)
);
vec4 aVec4 = vec4(0,0,0,1);
vec3 aVec3 = vec3(0,0,1);
// This is valid, and gives us (1,0,1)
vec3 outp = matrix * aVec4;
// This throws an exception:
// '*' : wrong operand types - no operation '*' exists that takes a left-hand operand of type 'highp 4X3 matrix of float' and a right operand of type 'highp 3-component vector of float' (or there is no acceptable conversion)
// vec4 outp = matrix * aVec3;
fragColor = vec4(outp,1.0);
}
If you multiply a 4x3 matrix with a vector, it must be a vec4 and the result will be a vec3.
@recp Per what I see from @Error-mdl We need more multiplication functions for #x# matrices.
Current we support, plus more
2x2 X 2x1 = 2x1 (vec2) 2x3 X 3x1 = 2x1 (vec2) 2x4 X 4x1 = 2x1 (vec2)
3x2 X 2x1 = 3x1 (vec3) 3x3 X 3x1 = 3x1 (vec3) 3x4 X 4x1 = 3x1 (vec3)
4x2 X 2x1 = 4x1 (vec4) 4x3 X 3x1 = 4x1 (vec4) 4x4 X 4x1 = 4x1 (vec4)
But we should add functions that do operations as described about
1x4 X 4x3 = 1x3 (vec3) 2x4 X 4x3 = 2x3
We honestly might as well add function for each #x# that does more types of multiplications.
Hi @Error-mdl, @EasyIP2023,
Wow, thanks for the catch, yes I think glm_mat4x3_mulv(mat4x3, vec3, vec4) must be glm_mat4x3_mulv(mat4x3, vec4, vec3).
Since we have only COLUMN vectors now we cant have both 1x4 ( which is vec4 ) and 4x1 ( ? ).
4x1 simply can be vec4 too as type but in functions it must have different representation to differ COL from ROW. For instance glm_mat4_mulv() as M * COL = COL, glm_mat4_mulvr() maybe but...
glm_mat4_mulv(mat4 m, vec4 v, vec4 dest)-> to M * COL = COLglm_vec4_mul4x4(vec4 v, mat4 m, vec4 dest)-> to ROW * M = ROW
in 1st matrix come first then vector in func name also in operation. In the second one vector come first then matrix as func name and operation. I like this if there are no better ideas, we can implement ROW * MAT as this style?
Thanks
EDIT: Actually we used the term "row" as vec4 in glm_mat4_rmc(vec4 r, mat4 m, vec4 c) which is
ROW * MAT * COL = SCALAR
I'm probably entirely wrong about this it just makes more since to me.
Sorry, in advance if i'm being annoying, but my mind just isn't fully understanding.
Would be nice If in the docs there were a table of some sort showing how each type vec2,vec3,vec4,mat4x3,mat4,mat2,etc.. gets laid/drawn out on paper.
Were my confusion lies is how is this laid out on paper. Also this would be good for people like me.
My mind is convinced that because GLSL spec swaps mat<row>x<col> to mat<col>x<row>
then on paper when we form/draw/lay out the matrix we also need to swap row and column.
Also, you can't multiply 4x3 X 4x1 or 4x3 X 1x4 as that's invalid linear algebra.
Laid out on paper mat4x3 with original matrix form
| column 1 | column 2 | column 3 | column 4 | |
|---|---|---|---|---|
| row 1 | m00 | m10 | m20 | m30 |
| row 2 | m01 | m11 | m21 | m31 |
| row 3 | m02 | m12 | m22 | m32 |
I was always told a good rule to remember is
mxn X nxp -> mxp
If we keep original matrix form and multiply by vec4
then the equation turns into:
* 4x3 X 4x1 (vec4) -> invalid
Even if vec4 isn't formed like above the equation still turns into:
* 4x3 X 1x4 -> invalid
My concern is that two inner numbers of the matrix
mxn X nxp don't match up.
If we keep original matrix form,
but multiply by vec3 then it becomes valid:
4x3 X 3x1 (vec3) -> 4x1 (vec4) [4 columns, 1 row]
Laid out on paper mat4x3 with swapped matrix form (DUE to GLSL)
| row 1 | row 2 | row 3 | |
|---|---|---|---|
| column 1 | m00 | m01 | m02 |
| column 2 | m10 | m11 | m12 |
| column 3 | m20 | m21 | m22 |
| column 4 | m30 | m31 | m32 |
mxn X nxp -> mxp
If we don't keep the original matrix form and multiply by vec3
then the equation turns into
4x3 X 3x1 (vec3) -> 4x1 (vec4) [4 columns, 1 row]
If we don't keep the original matrix form and multiply by vec4
then the equation turns into
4x3 X 4x1 (vec4) -> invalid
Sorry for poor documentation, we should clarify how cglm keep matrices on memory and how to access items.
cglm keeps matrices as COLUMN-MAJOR order in memory. It's all about how we define vectors and how to keep in memory.
A single vector is considered as a column, for instance vec4:
| vec4 (1x4) |
|---|
| X |
| Y |
| Z |
| W |
When we talk about a vector in general it is a COLUMN as above. But someone may store 4x1 which can be considered as row vector in vec4 for convenience. On the other hand matrices are consist from vectors. For instance an affine matrix looks like:
| column 1 | column 2 | column 3 | column 4 |
|---|---|---|---|
| Rx0 | Ry0 | Rz0 | Px |
| Rx1 | Ry1 | Rz1 | Py |
| Rx2 | Ry2 | Rz2 | Pz |
| 0 | 0 | 0 | W |
which consists from 4 column vectors: Rx, Ry, Rz and P column vectors. We keep them in memory as:
VEC1 | VEC2 | VEC3 | VEC4 COL1 | COL2 | COL3 | COL4
Matrix[m] will give you m vector or m column. Why COLUMN vectors? For instance; consider if you want to access position and make changes, it is easy to do this in this way ( store matrix as columns in memory ). Matrix[3] will give a vector that contains positions. This is also cache friendly and efficient. In ROW_MAJOR similar effect can be achieved by transposing the matrix:
| column 1 | column 2 | column 3 | column 4 | |
|---|---|---|---|---|
| row 1 | Rx0 | Rx1 | Rx2 | 0 |
| row 2 | Ry0 | Ry1 | Ry2 | 0 |
| row 3 | Rz0 | Rz1 | Rz2 | 0 |
| row 4 | Px | Py | Pz | W |
it is all about convention.
4x2:
M M M M
M M M M
2x4:
M M
M M
M M
M M
glm_mat4x2_mul(mat4x2 m1, mat2x4 m2, mat4 dest) seems give mat4, but it must mat2 in this case. I think we must re-check matrix * matrix and matrix * vector on non-square matrices 🤷♂️
Matrix(m, n) --> m columns, n rows not m rows n columns.
I think we must re-check matrix * matrix and matrix * vector on non-square matrices
If the intent is to also support vector * matrix on non-square matrices much of the current code could also be saved (or moved). However, what would be missing here are the ROW * M functions for square matrices.
Would there be any issue with naming the ROW * M functions as _vmul(...), _rvmul(...), or _vrmul(...)?
@gottfriedleibniz _vmul(...) seems nice choice for ROW * M
glm_mat4_mulv(mat4 m, vec4 v, vec4 dest)-> M * COLglm_mat4_vmul(vec4 v, mat4 m, vec4 dest)-> ROW * Mglm_vec4_mul4x4(vec4 v, mat4 m, vec4 dest)-> ROW * M
glm_mat4_vmul() and glm_vec4_mul4x4() can be alias of each other by inline function or macro.
@Error-mdl Good, catch. You're correct! This is making more since now @recp
- The rule
mxn X nxp -> mxp | rowxcol X rowxcol -> rowxcolholds true in normal linear algebra - The rule
nxm X pxn -> nxn | colxrow X colxrow -> colxrowholds true in GLSL linear algebra- if a matrix is multiplied by a matrix
- The rule
pxn X nxm -> pxm | colxrow X colxrow -> colxrowholds true in GLSL linear algebra- if a matrix is multiplied by a column vector
- The rule
mxn X nxp -> mxp | colxrow X colxrow -> colxrowholds true in GLSL linear algebra- if a matrix is multiplied by a row vector
Of course not actual rules, but it helps me.
Also maybe adding new types rvec2, rvec3, rvec4 instead of only using vec2,vec3,vec4 to represent row vectors would help me at least understand its row vector based operations being preformed.
Linear algebra: matrix * matrix
mat4x2 X mat2x4 -> mat4 | 4x2 X 2x4 -> 4x4 | mxn X nxp -> mxp
GLSL linear algebra: matrix * matrix
mat4x2 X mat2x4 -> mat2 | 2x4 X 4x2 -> 2x2 | nxm X pxn -> nxn
Linear algebra: matrix * matrix
mat4x3 X mat3x4 -> mat4 | 4x3 X 3x4 -> 4x4 | mxn X nxp -> mxp
GLSL linear algebra: matrix * matrix
mat4x3 X mat3x4 -> mat3 | 3x4 X 4x3 -> 3x3 | nxm X pxn -> nxn
GLSL linear algebra: matrix * row vector (rvec)
mat4x3 X rvec3 (3x1) -> rvec4 (4x1) | 4x3 X 3x1 -> 4x1 | mxn X nxp -> mxp
GLSL linear algebra: matrix * column vector (vec)
mat4x3 X vec4 (1x4) -> vec3 (1x3) | 1x4 X 4x3 -> 1x3 | pxn X nxm -> pxm
@EasyIP2023 I'm not sure about adding rvec2, rvec3, rvec4 types since these would only be type aliases and wont keep any info if it is row or col. On the other hand they may be readable where they are used e.g.
typedef struct transform_or_other_type {
rvec4 row1;
mat4x1 row2;
vec4 col1;
mat1x4 col2;
}
and something like maybe:
typedef vec4 mat1x4;
typedef vec4 mat4x1;
typedef mat4x1 rvec4;
which gives relation between mat4x1 and rvec4, user may prefer one which they like or meaningful in the context is used. Any feedbacks?
I would love that.
Would also like to use rvec4, etc... in function parameter for relevant functions.
@recp Have cycles
Wanted to wait until more feedback.
Do you want me to start converting? Or continue to wait for more feedback?
glm_mat#x#_mulv -> glm_mat#x#_vmul- Add new types (Can probably disregard. Up to you.)
typedef vec4 mat1x4;
typedef vec4 mat4x1;
typedef mat4x1 rvec4;
.....
.....
- Update docs to include tables
- Add corrected multiplication functions
Hi @EasyIP2023,
glm_mat#x#_mulv -> glm_mat#x#_vmu
Actually no need to converting IIRC, _mulv ( existing one ) will remain same as M * COL and _vmul will be new one as ROW * M. ( I fear they may be confused due to similar names. Parameter orders may be different maybe e.g. mat4,vec4 | vec4,mat4 )
Or continue to wait for more feedback?
Yes let's wait a little more please