Navigation

Tutoriel précédent : dessins indexés   Sommaire   Tutoriel suivant : projection en perspective

II. Contexte

Dans les derniers tutoriels, nous avions mis en place diverses transformations nous donnant la possibilité de déplacer un objet n'importe où dans un monde en 3D.

Nous en avons encore quelques-unes à apprendre (contrôle de caméra et projection en perspective), mais comme vous l'avez certainement deviné, une combinaison des transformations est nécessaire. Dans la plupart des cas, vous voudrez mettre un objet à l'échelle afin qu'il s'intègre correctement à votre monde 3D, le tourner dans l'orientation voulue, le déplacer quelque part, etc.

Jusqu'à présent, nous nous sommes exercés sur une seule transformation à la fois. Afin de mettre en place la série de transformations, nous devons multiplier la première matrice de transformation par la position du sommet, puis multiplier la matrice suivante par le résultat de cette multiplication, et ainsi de suite jusqu'à ce que toutes les matrices de transformation soient appliquées au sommet.

Une manière triviale de faire cela est de fournir toutes les matrices de transformation au shader et de le laisser faire les multiplications. Cependant, c'est très inefficace, car les matrices sont les mêmes pour tous les sommets et seule la position du sommet change. Par chance, l'algèbre linéaire fournit un ensemble de règles qui vont nous rendre la vie plus facile. Il est dit que pour un ensemble de matrices M0Mn et un vecteur V, l'équation suivante est vraie :

 
Sélectionnez
Mn * Mn-1 * ... * M0 * V = (Mn* Mn-1 * ... * M0) * V

Donc, si on calcule :

 
Sélectionnez
N = Mn * Mn-1 * ... * M0

alors :

 
Sélectionnez
Mn * Mn-1 * ... * M0 * V = N * V

Cela signifie que nous pouvons calculer N une fois, puis l'envoyer au shader via une variable uniforme où elle sera multipliée à chaque sommet. Cela requiert donc une seule multiplication matrice/vecteur par sommet au niveau du GPU.

Comment ordonner les matrices lors de la génération de N ? La première chose dont il faut se souvenir est que le vecteur est d'abord multiplié par la matrice la plus à droite de la série (dans notre cas M0). Puis, le vecteur est transformé par chaque matrice de droite à gauche. En graphisme 3D, vous voudrez généralement mettre l'objet à l'échelle, puis l'orienter, le déplacer, lui appliquer la transformation de caméra et enfin le projeter en 2D. Voyons ce qui arrive lorsque vous tournez avant de déplacer :

Image non disponible

Voyons maintenant ce qui arrive lorsque vous déplacez avant de tourner :

Image non disponible

Comme vous pouvez le voir, il est très difficile de définir la position de l'objet dans le monde s'il est d'abord déplacé, car si vous le déplacez de son point d'origine, puis que vous le tournez, il tourne autour de l'origine, ce qui signifie que vous le déplacez encore. Le second déplacement est une chose que vous voulez éviter. En le tournant avant de le déplacer, vous retirez la connexion entre les deux opérations. C'est pour cela qu'il est toujours mieux de modéliser un objet autour de l'origine de la manière la plus symétrique possible. De cette manière, lorsque plus tard vous mettrez à l'échelle ou tournerez votre objet, il n'y aura pas d'effet de bord et l'objet mis à l'échelle ou tourné restera aussi symétrique qu'auparavant.

Maintenant que nous commençons à manipuler plusieurs transformations, nous devons perdre l'habitude de mettre à jour la matrice directement dans la fonction de rendu. Cette méthode n'est pas évolutive et est source d'erreur. À la place, une classe de pipeline est introduite. Cette classe masque les détails des manipulations de matrice au travers d'une interface simple afin de changer le déplacement, la rotation, etc. Après avoir défini tous les paramètres, vous extrayez simplement la matrice combinant toutes les transformations. Cette matrice peut alors être directement donnée au shader.

III. Explication du code

 
Sélectionnez
#define ToRadian(x) ((x) * M_PI / 180.0f)
#define ToDegree(x) ((x) * 180.0f / M_PI)

Nous commençons à utiliser les valeurs réelles des angles dans ce tutoriel. Les fonctions de la bibliothèque standard du C prennent des radians en paramètres. Les macros ci-dessus prennent un angle soit en radians, soit en degrés et le convertissent dans l'autre notation.

 
Sélectionnez
inline Matrix4f operator*(const Matrix4f& Right) const
{
    Matrix4f Ret;
    for (unsigned int i = 0 ; i < 4 ; i++) {
        for (unsigned int j = 0 ; j < 4 ; j++) {
            Ret.m[i][j] = m[i][0] * Right.m[0][j] +
                m[i][1] * Right.m[1][j] +
                m[i][2] * Right.m[2][j] +
                m[i][3] * Right.m[3][j];
        }
    }

    return Ret;
}

Cet opérateur prend en charge la multiplication de matrices. Comme vous pouvez le voir, chaque valeur de la matrice résultat est définie comme le produit scalaire de sa ligne dans la matrice de gauche avec sa colonne de la matrice de droite. Cet opérateur est la clef de l'implémentation de la classe du pipeline.

 
Sélectionnez
class Pipeline
{
public:
    Pipeline()
    { ...  }

    void Scale(float ScaleX, float ScaleY, float ScaleZ)
    { ... }

    void WorldPos(float x, float y, float z)
    { ... }

    void Rotate(float RotateX, float RotateY, float RotateZ)
    { ... }

    const Matrix4f* GetTrans();
private:
    Vector3f m_scale;
    Vector3f m_worldPos;
    Vector3f m_rotateInfo;
    Matrix4f m_transformation;
};

Le pipeline cache les détails de la récupération de la combinaison de toutes les transformations requises pour un simple objet. Il y a actuellement trois vecteurs membres privés qui contiennent la mise à l'échelle, la position dans l'espace monde et la rotation sur chaque axe. Il y a en plus les interfaces pour les définir et une fonction pour récupérer la matrice représentant la somme de ces transformations.

 
Sélectionnez
const Matrix4f* Pipeline::GetTrans()
{
        Matrix4f ScaleTrans, RotateTrans, TranslationTrans;
        InitScaleTransform(ScaleTrans);
        InitRotateTransform(RotateTrans);
        InitTranslationTransform(TranslationTrans);
        m_transformation = TranslationTrans * RotateTrans * ScaleTrans;
        return &m_transformation;
}

Cette fonction initialise trois matrices différentes, une par transformation, contenant la configuration actuelle. Elle les multiplie une par une et retourne le produit final. Notez que l'ordre est codé en dur et suit la description ci-dessus. Si vous avez besoin de plus de flexibilité ici, vous pouvez utiliser un masque de bits qui spécifiera l'ordre. Notez aussi que la transformation finale est stockée en tant que membre. Vous pouvez essayer d'optimiser cette fonction en vérifiant un indicateur de modification et en retournant la matrice stockée s'il n'y a pas eu de modifications dans la configuration depuis la dernière fois que cette fonction a été appelée.

Cette fonction utilise des méthodes privées pour générer les différentes transformations à partir de ce que nous avons appris lors des précédents tutoriels. Dans les tutoriels suivants, cette classe sera étendue pour gérer les contrôles de caméra et la perspective de projection.

 
Sélectionnez
Pipeline p;
p.Scale(sinf(Scale * 0.1f), sinf(Scale * 0.1f), sinf(Scale * 0.1f));
p.WorldPos(sinf(Scale), 0.0f, 0.0f);
p.Rotate(sinf(Scale) * 90.0f, sinf(Scale) * 90.0f, sinf(Scale) * 90.0f);
glUniformMatrix4fv(gWorldLocation, 1, GL_TRUE, (const Glfloat*)p.GetTrans());

Voici les changements dans la fonction de rendu. Nous allouons un objet de pipeline, le configurons et envoyons la transformation résultante au shader. Vous pouvez jouer avec les paramètres pour voir leur effet sur l'image finale.

IV. Sources

Vous pouvez télécharger les sources de ce projet en suivant ce lien :

Récupérer les sources

V. Remerciements

Merci à Etay Meiri qui nous a permis de traduire son tutoriel.

Merci à LittleWhite pour ses corrections et à milkoseck pour sa relecture orthographique.

Navigation

Tutoriel précédent : dessins indexés   Sommaire   Tutoriel suivant : projection en perspective