Mathématiques 3D

Introduction aux matrices

Lorsque vous programmez en OpenGL ou dans toute autre API graphique, vous vous heurtez à un mur de briques lorsque vous n’êtes pas très bon en mathématiques. Ici, je vais vous expliquer avec un exemple de code comment vous pouvez réaliser un mouvement/mise à l’échelle et bien d’autres trucs sympas avec votre objet 3D.

Prenons un cas réel… Vous avez créé un cube impressionnant (en trois dimensions) dans OpenGL et vous voulez le déplacer dans n’importe quelle direction.

glUseProgram(cubeProgram)
glBindVertexArray(cubeVAO)
glEnableVertexAttribArray ( 0 );
glDrawArrays ( GL_TRIANGLES, 0,cubeVerticesSize)

Dans les moteurs de jeu comme Unity3d, ce serait facile. Vous appelleriez simplement transform.Translate() et en auriez fini, mais OpenGL n’inclut pas de bibliothèque mathématique.

Une bonne bibliothèque mathématique est [glm][1] mais pour faire passer mon message, je vais coder toutes les méthodes mathématiques (importantes) pour vous.

Il faut d’abord comprendre qu’un objet 3d en OpenGL contient beaucoup d’informations, il y a beaucoup de variables qui dépendent les unes des autres. Une façon intelligente de gérer toutes ces variables consiste à utiliser des matrices.

Une matrice est un ensemble de variables écrites en colonnes et en lignes. Une matrice peut être 1x1, 2x4 ou n’importe quel nombre arbitraire.

[1|2|3]
[4|5|6]
[7|8|9] //A 3x3 matrix

Vous pouvez faire des choses vraiment cool avec eux… mais comment peuvent-ils m’aider à déplacer mon cube ? Pour comprendre réellement cela, nous devons d’abord savoir plusieurs choses.

  • Comment fait-on une matrice à partir d’une position ?
  • Comment traduit-on une matrice ?
  • Comment le transmettez-vous à OpenGL ?

Créons une classe contenant toutes nos données et méthodes matricielles importantes (écrit en c++)

template<typename T>
//Very simple vector containing 4 variables
struct Vector4{
    T x, y, z, w;
    Vector4(T x, T y, T z, T w) : x(x), y(y), z(z), w(w){}
    Vector4(){}

    Vector4<T>& operator=(Vector4<T> other){
        this->x = other.x;
        this->y = other.y;
        this->z = other.z;
        this->w = other.w;
        return *this;
    }
}

template<typename T>
struct Matrix4x4{
     /*!
     *  You see there are columns and rows like this
     */
    Vector4<T> row1,row2,row3,row4;

    /*!
     *  Initializes the matrix with a identity matrix. (all zeroes except the ones diagonal)
     */
    Matrix4x4(){
        row1 = Vector4<T>(1,0,0,0);
        row2 = Vector4<T>(0,1,0,0);
        row3 = Vector4<T>(0,0,1,0);
        row4 = Vector4<T>(0,0,0,1);
    }

    static Matrix4x4<T> identityMatrix(){
        return Matrix4x4<T>(
                        Vector4<T>(1,0,0,0),
                        Vector4<T>(0,1,0,0),
                        Vector4<T>(0,0,1,0),
                        Vector4<T>(0,0,0,1));
    }



    Matrix4x4(const Matrix4x4<T>& other){
        this->row1 = other.row1;
        this->row2 = other.row2;
        this->row3 = other.row3;
        this->row4 = other.row4;
    }

    Matrix4x4(Vector4<T> r1, Vector4<T> r2, Vector4<T> r3, Vector4<T> r4){
        this->row1 = r1;
        this->row2 = r2;
        this->row3 = r3;
        this->row4 = r4;
    }

      /*!
     *  Get all the data in an Vector
     *  @return rawData The vector with all the row data
     */
    std::vector<T> getRawData() const{
        return{
            row1.x,row1.y,row1.z,row1.w,
            row2.x,row2.y,row2.z,row2.w,
            row3.x,row3.y,row3.z,row3.w,
            row4.x,row4.y,row4.z,row4.w
        };
    }

}

Nous remarquons d’abord une chose très particulière dans le constructeur par défaut d’une matrice 4 par 4. Lorsqu’il est appelé, il ne démarre pas tout à zéro mais ressemble à:

[1|0|0|0]
[0|1|0|0]
[0|0|1|0]
[0|0|0|1] //A identity 4 by 4 matrix

Toutes les matrices doivent commencer par des uns sur la diagonale. (juste parce que >.<)

Bon alors déclarons à notre cube épique une matrice 4 par 4.

glUseProgram(cubeProgram)
Matrix4x4<float> position;
glBindVertexArray(cubeVAO)
glUniformMatrix4fv(shaderRef, 1, GL_TRUE, cubeData);
glEnableVertexAttribArray ( 0 );
glDrawArrays ( GL_TRIANGLES, 0,cubeVerticesSize)

Maintenant que nous avons toutes nos variables, nous pouvons enfin commencer à faire des calculs ! Faisons la traduction. Si vous avez programmé dans Unity3d, vous vous souvenez peut-être d’une fonction Transform.Translate. Implémentons-le dans notre propre classe de matrice

 /*!
 *  Translates the matrix to
 *  @param vector, The vector you wish to translate to
 */
static Matrix4x4<T> translate(Matrix4x4<T> mat, T x, T y, T z){
    Matrix4x4<T> result(mat);
    result.row1.w += x;
    result.row2.w += y;
    result.row3.w += z;
    return result;
}

C’est tout le calcul nécessaire pour déplacer le cube (pas de rotation ou de mise à l’échelle, attention) Cela fonctionne sous tous les angles. Mettons cela en œuvre dans notre scénario réel

glUseProgram(cubeProgram)
Matrix4x4<float> position;
position = Matrix4x4<float>::translate(position, 1,0,0);
glBindVertexArray(cubeVAO)
glUniformMatrix4fv(shaderRef, 1, GL_TRUE, &position.getRawData()[0]);
glEnableVertexAttribArray ( 0 );
glDrawArrays ( GL_TRIANGLES, 0,cubeVerticesSize)

Notre shader doit utiliser notre merveilleuse matrice

#version 410 core
uniform mat4 mv_matrix;
layout(location = 0) in vec4 position;

void main(void){
    gl_Position = v_matrix * position;
}

Et cela devrait fonctionner…. mais il semble que nous ayons déjà un bogue dans notre programme. Lorsque vous vous déplacez le long de l’axe z, votre objet semble disparaître dans les airs. C’est parce que nous n’avons pas de matrice de projection. Pour résoudre ce bug, nous devons savoir deux choses :

  1. A quoi ressemble une matrice de projection ?
  2. Comment pouvons-nous le combiner avec notre matrice de position ?

Eh bien, nous pouvons créer une matrice de perspective (nous utilisons trois dimensions après tout) Le code

template<typename T>
Matrix4x4<T> perspective(T fovy, T aspect, T near, T far){
    
    T q = 1.0f / tan((0.5f * fovy) * (3.14 / 180));
    T A = q / aspect;
    T B = (near + far) / (near - far);
    T C = (2.0f * near * far) / (near - far);
    
    return Matrix4x4<T>(
        Vector4<T>(A,0,0,0),
        Vector4<T>(0,q,0,0),
        Vector4<T>(0,0,B,-1),
        Vector4<T>(0,0,C,0));
}

Cela semble effrayant, mais cette méthode calculera en fait une matrice de la distance à laquelle vous souhaitez regarder dans la distance (et de la distance) et de votre champ de vision.

Nous avons maintenant une matrice de projection et une matrice de position. Mais comment les combiner ? Ce qui est amusant, c’est que nous pouvons en fait multiplier deux matrices l’une avec l’autre.

/*!
 *  Multiplies a matrix with an other matrix
 *  @param other, the matrix you wish to multiply with
 */
static Matrix4x4<T> multiply(const Matrix4x4<T>& first,const Matrix4x4<T>& other){
    //generate temporary matrix
    Matrix4x4<T> result;
    //Row 1
    result.row1.x = first.row1.x * other.row1.x + first.row1.y * other.row2.x + first.row1.z * other.row3.x + first.row1.w * other.row4.x;
    result.row1.y = first.row1.x * other.row1.y + first.row1.y * other.row2.y + first.row1.z * other.row3.y + first.row1.w * other.row4.y;
    result.row1.z = first.row1.x * other.row1.z + first.row1.y * other.row2.z + first.row1.z * other.row3.z + first.row1.w * other.row4.z;
    result.row1.w = first.row1.x * other.row1.w + first.row1.y * other.row2.w + first.row1.z * other.row3.w + first.row1.w * other.row4.w;
    
    //Row2
    result.row2.x = first.row2.x * other.row1.x + first.row2.y * other.row2.x + first.row2.z * other.row3.x + first.row2.w * other.row4.x;
    result.row2.y = first.row2.x * other.row1.y + first.row2.y * other.row2.y + first.row2.z * other.row3.y + first.row2.w * other.row4.y;
    result.row2.z = first.row2.x * other.row1.z + first.row2.y * other.row2.z + first.row2.z * other.row3.z + first.row2.w * other.row4.z;
    result.row2.w = first.row2.x * other.row1.w + first.row2.y * other.row2.w + first.row2.z * other.row3.w + first.row2.w * other.row4.w;
    
    //Row3
    result.row3.x = first.row3.x * other.row1.x + first.row3.y * other.row2.x + first.row3.z * other.row3.x + first.row3.w * other.row4.x;
    result.row3.y = first.row3.x * other.row1.y + first.row3.y * other.row2.y + first.row3.z * other.row3.y + first.row3.w * other.row4.y;
    result.row3.z = first.row3.x * other.row1.z + first.row3.y * other.row2.z + first.row3.z * other.row3.z + first.row3.w * other.row4.z;
    result.row3.w = first.row3.x * other.row1.w + first.row3.y * other.row2.w + first.row3.z * other.row3.w + first.row3.w * other.row4.w;
    
    //Row4
    result.row4.x = first.row4.x * other.row1.x + first.row4.y * other.row2.x + first.row4.z * other.row3.x + first.row4.w * other.row4.x;
    result.row4.y = first.row4.x * other.row1.y + first.row4.y * other.row2.y + first.row4.z * other.row3.y + first.row4.w * other.row4.y;
    result.row4.z = first.row4.x * other.row1.z + first.row4.y * other.row2.z + first.row4.z * other.row3.z + first.row4.w * other.row4.z;
    result.row4.w = first.row4.x * other.row1.w + first.row4.y * other.row2.w + first.row4.z * other.row3.w + first.row4.w * other.row4.w;
    
    return result;
}

Ooef .. c’est beaucoup de code qui a l’air plus effrayant qu’il n’y paraît. Cela peut être fait dans une boucle for mais j’ai (probablement à tort) pensé que ce serait plus clair pour les personnes qui n’ont jamais travaillé avec des matrices.

Regardez le code et remarquez un motif répétitif. Multipliez la colonne avec la ligne, ajoutez-la et continuez (c’est la même chose pour n’importe quelle taille de matrice)

*Notez que la multiplication avec des matrices n’est pas comme la multiplication normale. UNE X B != B x UNE *

Maintenant que nous savons comment projeter et ajouter ceci à notre matrice de position, notre code réel ressemblera probablement à :

glUseProgram(cubeProgram)
Matrix4x4<float> position;
position = Matrix4x4<float>::translate(position, 1,0,0);
position = Matrix4x4<float>::multiply(Matrix<float>::perspective<float>(50, 1 , 0.1f, 100000.0f), position);
glBindVertexArray(cubeVAO)
glUniformMatrix4fv(shaderRef, 1, GL_TRUE, &position.getRawData()[0]);
glEnableVertexAttribArray ( 0 );
glDrawArrays ( GL_TRIANGLES, 0,cubeVerticesSize)

Maintenant, notre bug est écrasé et notre cube a l’air assez épique au loin. Si vous souhaitez mettre à l’échelle votre cube, la formule est la suivante :

 /*!
 *  Scales the matrix with given vector
 *  @param s The vector you wish to scale with
 */
static Matrix4x4<T> scale(const Matrix4x4<T>& mat, T x, T y, T z){
    Matrix4x4<T> tmp(mat);
    tmp.row1.x *= x;
    tmp.row2.y *= y;
    tmp.row3.z *= z;
    return tmp;
}

Il vous suffit d’ajuster les variables diagonales.

Pour la rotation, vous devez regarder de plus près les Quaternions.

[1] : http://glm.g-truc.net/0.9.7/index.html