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 M0…Mn et un vecteur V, l'équation suivante est vraie :
Mn *
Mn-
1
*
... *
M0 *
V =
(Mn*
Mn-
1
*
... *
M0) *
V
Donc, si on calcule :
N =
Mn *
Mn-
1
*
... *
M0
alors :
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 :
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 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▲
#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 interfaces 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.1
f), sinf(Scale *
0.1
f), sinf(Scale *
0.1
f));
p.WorldPos(sinf(Scale), 0.0
f, 0.0
f);
p.Rotate(sinf(Scale) *
90.0
f, sinf(Scale) *
90.0
f, sinf(Scale) *
90.0
f);
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 :
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 |