Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-square matrix * column vector multiplication implemented as row vector * matrix multiplication #385

Open
Error-mdl opened this issue Feb 9, 2024 · 13 comments

Comments

@Error-mdl
Copy link

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:

glm_mat4x3_mulv(mat4x3 m, vec3 v, vec4 dest) {

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)

dest[0] = m[0][0] * v0 + m[0][1] * v1 + m[0][2] * v2;

The square matrix types correctly dot the rows of the matrix with the vector to get each component of the output:

res[0] = m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2] + m[3][0] * v[3];

@EasyIP2023
Copy link
Contributor

@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

glm_mat4x3_mulv(mat4x3 m, vec3 v, vec4 dest) {

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

253818963-5ddc8e77-00fe-403e-a535-8c52c8770026

@Error-mdl
Copy link
Author

From the openGL 4.60 spec page 126:

image

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.

@EasyIP2023
Copy link
Contributor

EasyIP2023 commented Feb 10, 2024

@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.

@recp
Copy link
Owner

recp commented Feb 10, 2024

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...

  1. glm_mat4_mulv(mat4 m, vec4 v, vec4 dest) -> to M * COL = COL
  2. glm_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

@EasyIP2023
Copy link
Contributor

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

@recp
Copy link
Owner

recp commented Feb 11, 2024

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.

@gottfriedleibniz
Copy link

gottfriedleibniz commented Feb 11, 2024

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(...)?

@recp
Copy link
Owner

recp commented Feb 11, 2024

@gottfriedleibniz _vmul(...) seems nice choice for ROW * M

  • glm_mat4_mulv(mat4 m, vec4 v, vec4 dest) -> M * COL
  • glm_mat4_vmul(vec4 v, mat4 m, vec4 dest) -> ROW * M
  • glm_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.

@EasyIP2023
Copy link
Contributor

@Error-mdl Good, catch. You're correct! This is making more since now @recp

  • The rule mxn X nxp -> mxp | rowxcol X rowxcol -> rowxcol holds true in normal linear algebra
  • The rule nxm X pxn -> nxn | colxrow X colxrow -> colxrow holds true in GLSL linear algebra
    • if a matrix is multiplied by a matrix
  • The rule pxn X nxm -> pxm | colxrow X colxrow -> colxrow holds true in GLSL linear algebra
    • if a matrix is multiplied by a column vector
  • The rule mxn X nxp -> mxp | colxrow X colxrow -> colxrow holds 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 

@recp
Copy link
Owner

recp commented Feb 12, 2024

@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?

@EasyIP2023
Copy link
Contributor

I would love that.

Would also like to use rvec4, etc... in function parameter for relevant functions.

@EasyIP2023
Copy link
Contributor

@recp Have cycles

Wanted to wait until more feedback.

Do you want me to start converting? Or continue to wait for more feedback?

  1. glm_mat#x#_mulv -> glm_mat#x#_vmul
  2. Add new types (Can probably disregard. Up to you.)
typedef vec4   mat1x4;
typedef vec4   mat4x1;
typedef mat4x1 rvec4;
.....
.....
  1. Update docs to include tables
  2. Add corrected multiplication functions

@recp
Copy link
Owner

recp commented Mar 31, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants