This page is at least 10 years old !
Les transformations 3D
Todo: Tri en Z, calcul de temps de rendu en cycles, virer les fillstyle en trop.
On veut faire tourner un objet (groupe de points) dans un espace 3D.
Pour donner une bonne représentation visuelle tout en restant simple, prenons comme objet un cube dont on ne va garder que les sommets, soit 8 points. On ne tracera pas les arrêtes.
Très souvent, les axes d'un espace 3D sont nommés X, Y, et Z. Sans transformation, on aura le plan (X,Y) face au plan de la camera: X à l'horizontale, Y à la verticale, et Z sera la profondeur.
Si on centre les 8 points du cube sur le plan (X, Y), on obtient les coordonnées X,Y,Z suivantes:
Premier carré, devant nous (Z = -1):
- -1,-1,-1 (en haut à gauche)
- 1,-1,-1 (en haut à droite)
- 1,1,-1 (en bas à droite)
- -1,1,-1 (en bas à gauche)
Deuxième carré, derrière le premier (Z = 1):
- -1,-1,1 (en haut à gauche)
- 1,-1,1 (en haut à droite)
- 1,1,1 (en bas à droite)
- -1,1,1 (en bas à gauche)
Pour projeter des points 3D sur un plan 2D (l'écran), on peut soit simplement ignorer Z (projection orthogonale), soit diviser les coordonnées X et Y par Z (perspective):
px = cx + ((x * f) / (z + 2.5))
py = cy + ((y * f) / (z + 2.5))
- px et py sont les coordonnées cartésiennes "compatibles écran" qui serviront à dessiner les points
- cx et py définissent la position centrale du rendu, ils sont égaux à f dans les exemples
- f est le facteur d'agrandissement, 64 dans les exemples
- 2.5 est une valeur arbitraire pour rendre Z toujours positif et "avancé devant la caméra", sinon la caméra serait au centre du cube
Pour transformer les points, il faut partir de leurs coordonnées initiales et appliquer les unes après les autres les transformations pour chaque axe de rotation. Les angles seront appelés A pour l'axe Z, B pour l'axe Y, et C pour l'axe X.
Une rotation 3D sur tous les axes se décompose en 3 rotations 2D, dont l'ordre n'importe pas:
La rotation 2D sur l'axe Z, ne modifiera que les coordonnées X et Y.
Prenons A comme angle de rotation.
X et Y sont les coordonnées d'origine du point, XX et YY les nouvelles.
Matrice :
x | y | |
xx | cos | -sin |
yy | sin | cos |
xx = (cos(a)*x) - (sin(a)*y)
yy = (sin(a)*x) + (cos(a)*y)
La rotation 2D sur l'axe Y, ne modifiera que les coordonnées X et Z.
Prenons B comme angle de rotation.
X et Z sont les coordonnées précédentes du point, XX et ZZ les nouvelles.
Matrice :
x | z | |
xx | cos | sin |
zz | -sin | cos |
xx = (cos(b)*x) + (sin(b)*z)
zz = -(sin(b)*x) + (cos(b)*z)
La rotation 2D sur l'axe X, ne modifiera que les coordonnées Y et Z.
Prenons C comme angle de rotation.
Y et Z sont les coordonnées précédentes du point, YY et ZZ les nouvelles.
Matrice :
y | z | |
yy | cos | -sin |
zz | sin | cos |
yy = (cos(c)*y) - (sin(c)*z)
zz = (sin(c)*y) + (cos(c)*z)
Rotation sur Z, on va avoir un nouveaux X (XX) et Y (YY):
xx = (cos(a)*x) - (sin(a)*y)
yy = (sin(a)*x) + (cos(a)*y)
Rotation sur Y à partir de XX et Z. Nouveaux XX (XXX) et Z (ZZ):
xxx = (cos(b)*xx) + (sin(b)*z)
zz = (cos(b)*z) - (sin(b)*xx)
Rotation sur X à partir de YY et ZZ. Nouveaux YY (YYY) et ZZ (ZZZ):
yyy = (cos(c)*yy) - (sin(c)*zz)
zzz = (sin(c)*yy) + (cos(c)*zz)
Il ne reste plus qu'à projeter les coordonnées 3D XXX, YYY et ZZZ sur un plan 2D afin de faire le rendu:
px = cx + ((xxx * f) / (zzz + 2.5))
py = cy + ((yyy * f) / (zzz + 2.5))
Rendu en projection orthogonale (pas de distorsion en fonction de la profondeur) |
La profondeur (zzz) peut être utilisée comme luminance pour donner un effet de brouillard (plus le point est loin, plus il est sombre)... |
...Ou comme facteur d'agrandissement ou de réduction pour les points. |
Optimisations
Sur un ordinateur actuel, le temps que prennent de tels calculs est largement négligeable même sur des milliers de points. Par contre, si on recule dans le temps et qu'on se situe a l'époque ou les processeurs venaient juste d'être dotes d'instructions de multiplication, les temps deviennent beaucoup plus conséquents.
Comme les transformations doivent être effectuées pour chaque point, on multiplie le temps de calcul par le nombre de points. Pour minimiser cela, on va chercher a réduire le temps que prend le calcul d'un point. Comme les fonctions cosinus et sinus sont typiquement réalisées grâce a des tables précalculées, ce sont au final les multiplications qui prennent le plus de temps. On va donc tacher d'en faire le moins possible. Jusqu'ici, on a 12 multiplications par point (4 par axe, avec 3 axes).
Si on développe les précédents calculs, on obtient:
xx = x*[cos(a) * cos(b)] + y*[sin(a) * cos(b)] + z*sin(b)
yy = x*[sin(a) * cos(c) + cos(a) * sin(b) * sin(c)]
+ y*[-cos(a) * cos(c) + sin(a) * sin(b) * sin(c)]
+ z*[-cos(b) * sin(c)]
zz = x*[sin(a) * sin(c) - cos(a) * sin(b) * cos(c)]
+ y*[-cos(a) * sin(c) - sin(a) * sin(b) * cos(c)]
+ z*[cos(b) * cos(c)]
Sachant que les angles A, B et C ne vont pas changer pendant que les transformations pour une image donnée sont faites, on peut précalculer les facteurs qui en dépendent qu'une seule fois par image:
xxp = cos(a) * cos(b)
xyp = sin(a) * cos(b)
xzp = sin(b)
yxp = sin(a) * cos(c) + cos(a) * sin(b) * sin(c)
yyp = -cos(a) * cos(c) + sin(a) * sin(b) * sin(c)
yzp = -cos(b) * sin(c)
zxp = sin(a) * sin(c)) - cos(a) * sin(b) * cos(c)
zyp = -cos(a) * sin(c)) - sin(a) * sin(b) * cos(c)
zzp = cos(b) * cos(c)
Pour chaque point, on se retrouve avec les formules suivantes:
xx = x*xxp + y*xyp + z*xzp
yy = x*yxp
+ y*yyp
+ z*yzp
zz = x*zxp
+ y*zyp
+ z*zzp
Ce qui donne 9 multiplications par point (gain de 25%) et 16 initiales.
Ca fonctionne toujours :
On peut aller plus loin en remarquant que la formule pour chaque point ressemble à ax + by + cz.
Si on développe (a+y)(b+x) on obtient ab + ax + by + xy.
On souhaite garder ax + by, on soustrait donc (ab + xy) pour obtenir ax + by = (a+y)(b+x) - (ab + xy).
Il ne manque que cz, qu'on ajoute simplement: ax + by + cz = (a+y)(b+x) - (ab + xy) + cz.
Pour chaque coordonnée, on obtient:
xx = (xxp + y)(xyp + x) - (xxp*xyp + x*y) + z*zxp
yy = (yxp + y)(yyp + x) - (yxp*yyp + x*y) + z*yxp
zz = (zxp + y)(zyp + x) - (zxp*zyp + x*y) + z*zxp
On repasse a 12 multiplications, mais on peut voir que 3 d'entres elles ne dépendent que des angles et pas des coordonnées du point, on peut donc les précalculer pour tous les points, une fois par image comme ci-dessus:
xxp_xyp = xxp*xyp
yxp_yyp = yxp*yyp
zxp_zyp = zxp*zyp
On peut aussi voir que x*y est utilise 3 fois, on peut donc le calculer une seule fois pour le point:
x_y = x*y
z*zxp est utilisé deux fois:
z_zxp = z*zxp
On arrive donc, pour chaque point, a:
xx = (xxp + y)(xyp + x) + z_zxp - (xxp_xyp + x_y)
yy = (yxp + y)(yyp + x) + z*yxp - (yxp_yyp + x_y)
zz = (zxp + y)(zyp + x) + z_zxp - (zxp_zyp + x_y)
7 multiplications par point (gain de 41%) et 19 initiales.
Ca fonctionne toujours aussi bien !