Tutoriel 11 - Accumulation de transformations

Par OGLdev, traduit par DragonJoker

Introduction

Dans ce tutoriel, vous allez apprendre à combiner vos matrices de transformation pour les appliquer à vos objets.

Contexte

Dans les derniers tutoriels, nous avons 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, vous vous êtes exercé sur une 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 aient été 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 M0…Mn et un vecteur V, l'équation suivante est vraie :

\begin{equation*} M_n \times M_{n-1} \times ... \times M_0 \times V = (M_n \times M_{n-1} \times ... \times M_0) \times V \end{equation*}

Donc si on calcule :

\begin{equation*} N = M_n \times M_{n-1} \times ... \times M_0 \end{equation*}

Alors :

\begin{equation*} M_n \times M_{n-1} \times ... \times M_0 \times V = N \times V \end{equation*}

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

Comment ordonnancer 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 d'abord 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 :

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

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 quelque 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 mettez à l'échelle ou tournez votre objet, il n'y a pas d'effet de bord et l'objet mis à l'échelle ou tourné reste aussi symétrique qu'auparavant.

Maintenant que nous commençons à manipuler plus qu'une transformation, 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. A la place, une classe de pipeline est introduite. Cette classe masque les détails des manipulations de matrice au travers une API 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 transformation. Cette matrice peut alors être directement donnée au shader.

Explication du code

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

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.

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 API pour les définir et une fonction pour récupérer la matrice représentant la somme de ces transformations.

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.

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.

Remerciements

Merci à Etay Meiri de me permettre de traduire ses tutoriels.

Résultat :
resultat

Article d'origine