FlashPascal 3D
Cet article sur vous propose une explication de source sur un exemple de FlashPascal2 qui dessine des cubes en 3D.
Flash et la 3D
En premier lieu, il est nécessaire de préciser que FlashPascal utilisant une machine virtual Flash version 2 (correspondant à Flash 8), la 3D n'est pas supportée nativement. Le moteur de rendu graphique ne permet de produire que des effets 2D.Mais qu'à cela ne tienne, avec un peu de mathématiques, nous ferons une projection de la 3D vers la 2D gérée par Flash.
Un peu de mathématiques
Dans notre exemple, nous nous contenterons de faire tourner un cube sur lui même, il est donc question de Rotation vectorielle. Mais bon, le but n'étant pas ici de faire un cours magistral de mathématiques, nous irons au plus simple.Pour exprimer une rotation, nous utilisons la notion d'Angle. Exprimé en degrés, un tour complet représente 360°, un demi tour 180°, et quart de tour 90° (l'angle droit), etc. Mais en informatique on utilise généralement un autre unité, le Radian, qui n'est pas plus compliqué que le degré, il utilise simplement une autre valeur de référence: π (Pi), soit environ 3,141592654. Un tour complet c'est
2*PI
, un demi tour PI
, un quart de tour PI/2
. D'où la formule Radian = Degre * PI/180
.Pourquoi Pi ? c'est une bonne question, mais je passerais à côté, sachez juste que c'est un nombre magique qui a la particularité de ne pouvoir s'écrire autrement que π. En effet toute autre notation comme 3.14592654, ou 22/7 ne sont que des approximations car π a un nombre infinie de chiffres après la décimale.
Bref, j'ai donc une rotation exprimée en radian (ou en degré peu importe), comment cela va-il influencer mon cube ? Pour le savoir, il faut connaître deux fonctions mathématiques complémentaires, le Sinus et le Cosinus.
J'ai résumé visuellement ces deux fonctions dans l'animation ci-dessous
Le trait blanc tourne autour du centre du cercle vert. Les lignes rouges représentent le sinus et le cosinus de l'ange. Ce sont donc deux valeurs qui évoluent de -1 à +1 (on considère que le cercle vert représente "1") en boucle. En effet
Sin(0) = Sin(2*PI)
. La courbe jaune en bas représente l'évolution du sinus de l'angle au cours du temps sur la durée de deux rotations. La courbe violette c'est le cosinus. Vous remarquerez une certaine similitude entre les deux, c'est lié aux deux symétries du cercle, tous les quarts de cercle on peut inverser le sinus et le cosinus en changeant de signe au besoin.Et voilà, c'est tout ! C'est tout ? Oui, oui j'en ai fini avec les maths compliquées :)
Mais concrètement à quoi ça sert ? Pour dessiner la ligne blanche qui tourne, j'ai deux fonctions FlashPascal
moveTo
et lineTo
, je dois juste connaître les deux extrémités du trait pour le dessiner. Et justement, je connais le centre qui ne bouge pas, mais qu'elles sont les coordonnées du bout qui bouge ?Prenons les cas limites, au départ le trait est horizontal, disons 200 pixels:
moveTo(0,0); lineTo(0,200)
. Au bout d'un quart de tour c'est facile aussi moveTo(0, 0); lineTo(200, 0)
! Ok, mais entre le deux, la diagonale à 45° (ou Pi/4), comment je la trace ?C'est là qu'interviennent nos amis sinus et cosinus; si vous regardez les traits rouges, c'est eux qui me donne la position du point !
A = 45 * Math.PI/180; // 45° exprimé en radians cs := 200 * cos(A); // le cosinus pour un trait de 200 pixels sn := 200 * sin(A); // le sinus pour un trait de 200 pixels moveTo(0, 0); // le point de départ lineTo(cs, sn); // le point d'arrivé
Simple non ? Et ça marche évidemment dans tous les cas, si l'angle vaux 0, sinus = 0, cosinus = 1, on retrouve bien notre
lineTo(0, 200)
; si l'angle vaux 90°, sinus = 1, cosinus = 0, on retrouve encore notre lineTo(200, 0)
.Rotation en 3D
Bon ok c'est bien joli, mais la rotation en 3D ça doit être plus compliqué ? En fait non, en 3 dimensions la rotation s'effectue autour d'un axe qui n'affecte que deux des trois coordonnées.Regardez bien tourner ce cube, vous verrez que la hauteur des points ne changes pas (en 3 dimensions, nous allons voir après la projection). Les formules ne sont donc pas plus compliquées, elles s'appliquent simplement différemment en fonction de l'axe de rotation.
// rotation sur l'axe des X y1 := cos(a) * y - sin(a) * z; z1 := sin(a) * y + cos(a) * z; // rotation sur l'axe des y x1 := cos(a) * x + sin(a) * z; z1 := -sin(a) * x + cos(a) * z; // rotation sur l'axe des z x1 := cos(a) * x - sin(a) * y; y1 := sin(a) * x + cos(a) * y;
Pour avoir une rotation plus complexe, il suffira de combiner ces rotations de bases
Le cube ci-dessus subit une rotation de PI/4 en X, puis Y et enfin Z.
Le calcul matriciel
Nous savons maintenant comment faire subir tout un tas de transformations à un point 3D. Il faudra faire ces calculs sur l'ensemble des points de l'univers, un cube en ce qui nous concerne. Mais cette opération peut prendre du temps, on va donc utiliser un autre outil mathématique précieux pour nous faciliter la vie: la matrice.Au départ, les matrices ne sont qu'une notation particulière d'une équation. Nous avons vu que pour calculer une rotation selon un axe donné il faut multiplier x, y et z par certains facteurs. La notation matricielle de ces équations harmonise les choses; quand une variable n'est pas impliquée dans l'équation son facteur est nul.
type TMatrice: array[0..2, 0..2] of Single; var Matrice: TMatrice; begin // Matrice de rotation selon l'axe des X Matrice[0, 0] := 1; Matrice[0, 1] := 0; Matrice[0, 2] := 0; Matrice[1, 0] := 0; Matrice[1, 1] := cos(a); Matrice[1, 2] := -sin(a); Matrice[2, 0] := 0; Matrice[2, 1] := sin(a); Matrice[2, 2] := cos(a); x1 := Matrice[0, 0] * x + Matrice[0, 1] * y + Matrice[0, 2] * z; y1 := Matrice[1, 0] * x + Matrice[1, 1] * y + Matrice[1, 2] * z; z1 := Matrice[2, 0] * x + Matrice[2, 1] * y + Matrice[2, 2] * z; end;
en plaçant les facteurs dans une matrice, on peut appliquer bêtement et méchamment une transformation sur un point sans se préoccuper de la nature de cette transformation. Vous noterez qu'en utilisant (1,0,0) sur la première ligne, on obtient
x1 = x
ce qui est tout à fait normale pour une rotation sur X.On peut ajouter une colonne à notre matrice pour gérer la translation :
type TMatrice: array[0..2, 0..3] of Single; var Matrice: TMatrice; begin x1 := Matrice[0, 0] * x + Matrice[0, 1] * y + Matrice[0, 2] * z + Matrice[0, 3]; y1 := Matrice[1, 0] * x + Matrice[1, 1] * y + Matrice[1, 2] * z + Matrice[1, 3]; z1 := Matrice[2, 0] * x + Matrice[2, 1] * y + Matrice[2, 2] * z + Matrice[2, 3]; end;
Mais là ou les matrices deviennent intéressantes, c'est que l'on va pouvoir combiner les matrices. Au lieu de calculer une rotation selon X de tous les points de notre univers, puis de calculer une rotation selon Y de ces mêmes points, nous allons combiner les deux matrices de rotation dans une nouvelle matrice qui effectuera les deux rotations d'un seul coup sur chaque point.
voici comment on combine deux matrices M1 et M2 dans la matrice M:
for i := 0 to 3 do for j := 0 to 3 do M[i, j] := M1[i,0] * M2[0,j] + M1[i,1] * M2[1,j] + M1[i,2] * M2[2,j] + M1[i,3] * M2[3,j];
Pour calculer une rotation selon X puis une rotation selon Y on écrira donc:
type TMatrice: array[0..3, 0..3] of Single; var M1, M2, M :TMatrice; i, j: Integer; begin // Matrice de rotation selon l'axe des X M1[0, 0] := 1; M1[0, 1] := 0; M1[0, 2] := 0; M1[0, 3] := 0; M1[1, 0] := 0; M1[1, 1] := cos(a); M1[1, 2] := -sin(a); M1[1, 3] := 0; M1[2, 0] := 0; M1[2, 1] := sin(a); M1[2, 2] := cos(a); M1[2, 3] := 0; M1[3, 0] := 0; M1[3, 1] := 0; M1[3, 2] := 0; M1[3, 3] := 1; // Matrice de rotation selon l'axe des Y M1[0, 0] := cos(b); M1[0, 1] := 0; M1[0, 2] := sin(b); M1[0, 3] := 0; M1[1, 0] := 0; M1[1, 1] := 1; M1[1, 2] := 0; M1[1, 3] := 0; M1[2, 0] := -sin(b); M1[2, 1] := 0; M1[2, 2] := cos(b); M1[2, 3] := 0; M1[3, 0] := 0; M1[3, 1] := 0; M1[3, 2] := 0; M1[3, 3] := 1; // On combine les deux matrices for i := 0 to 3 do for j := 0 to 3 do M[i, j] := M1[i,0] * M2[0,j] + M1[i,1] * M2[1,j] + M1[i,2] * M2[2,j] + M1[i,3] * M2[3,j]; // Appliquer la matrice M revient à appliquer successivement les matrices M1 et M2 x1 := M[0, 0] * x + M[0, 1] * y + M[0, 2] * z + M[0, 3]; y1 := M[1, 0] * x + M[1, 1] * y + M[1, 2] * z + M[1, 3]; z1 := M[2, 0] * x + M[2, 1] * y + M[2, 2] * z + M[2, 3]; end;
Et on peut évidemment combiner autant de matrice que l'on veux avant d'appliquer la transformation finale aux points de notre univers. Les matrices apportent donc un énorme gain en terme de temps de calcul.
J'en ai presque terminé avec les matrices, il me faut vous préciser que la matrice nulle ou "identique" est la matrice qui ne fait aucune transformation : x1 = x, y1 = y et z1 = z, elle se notera naturellement (quand on n'oublie pas qu'elle contient des facteurs de x, y et z) :
1, 0, 0, 0
0, 1, 0, 0
0, 0, 1, 0
0, 0, 0, 1
Passage de la 3D à la 2D
On sait maintenant comment faire tourner un point dans l'espace, mais il nous reste une étape à franchir dans cette exploration de la 3D : la projection.Même les derniers écrans 3D sont en réalités des images 2D, une pour chaque oeil, c'est notre cerveau qui va leur donner du volume. On va donc devoir appliquer sur la surface plane de notre écran un dessin 2D représentant notre espace en 3 dimensions.
Il existe au moins deux façon de représenter la 3D, le cube vert de gauche est en représentation isométrique; c'est celle qu'on trouvera sur les plans de maquettes, de Lego ou de meubles suédois. Elle est physiquement fausse mais présente l'avantage d'être simple à réaliser.
Celle qui nous intéresse est la seconde, c'est une représentation réaliste de la 3D avec un point de fuite. Plus un objet est loin, plus il est petit. Le cube de droite est donc "déformé" par l'effet de profondeur...tout comme le sont les objets du monde réel.
Cet effet de profondeur est en fait très simple à obtenir, il suffit de diviser les coordonnées 3D x et y par la profondeur z. On prendre soins de placer les objets de telle sorte qu'on ne calcule jamais une division par 0, et on appliquera un facteur de grossissement qui correspond à l'angle de vue (la focale).
Mise en pratique
Si on en revient à notre démo FlashPascal de Cube en 3D dimensions, nous avons maintenant tous les éléments nécessaires pour comprendre le code...ou presque.Pour toutes les transformations 3D nous avons l'objet
TMatrix3d
type TMatrix3d = class Values: array[0..15] of Double; // facteurs courants de la matrice M : array[0..15] of Double; // Matrice identique utilisée pendant les calculs V : array[0..15] of Double; // Matrice temporaire utilisée pendant les calculs x,y,z : Double; // le point 3D transformé constructor Create; procedure Mult(); // Combine Values et M dans V, puis copie V dans Values procedure Translate(tx, ty, tz: Double); // Applique une translation 3D procedure RotateX(A: Double); // Applique une rotation selon l'axe des X procedure RotateY(A: Double); // Applique une rotation selon l'axe des Y procedure Transform(ax, ay, az: Double); // Applique les transformations à un point 3D procedure Transform2d(ax, ay: Double); // Calcule la projection 2D du point de l'espace 3D end;
Les matrices M et V sont des variables de travail qui évitent d'initialiser une matrice pour chaque calcul.
procedure TMatrix3d.Translate(tx, ty, tz: Double); begin M[12] := tx; M[13] := ty; M[14] := tz; Mult(); M[12] := 0; M[13] := 0; M[14] := 0; end;
Translate
utilise la matrice identique M pour indiquer une translation, après un appel à Mult
qui se charge de la combinaison, elle remet les valeurs initiales dans M pour la prochaine transformation.La méthode
Transform2d
a la particularité de transformer en coordonnées écran, un point 3D dont la coordonnées Z est nulle.procedure TMatrix3d.Transform2d(ax, ay: Double); begin x := ax * Values[0] + ay * Values[4] + {az * Values[ 8] +} Values[12]; y := ax * Values[1] + ay * Values[5] + {az * Values[ 9] +} Values[13]; z := ax * Values[2] + ay * Values[6] + {az * Values[10] +} Values[14]; x := 128 * x / (128 + z / 4); y := 128 * y / (128 + z / 4); end;
constructor TFace.Create(Parent: MovieClip; Depth, ASize: Integer; Rx, Ry: Double); begin inherited Create(Parent, 'face' + IntToStr(Depth), Depth); Size := ASize; Matrix := TMatrix3d.Create; Matrix.Translate(0, 0, ASize); // on déplace la face afin de donner une épaisseur au dé Matrix.RotateX(Rx); // puis on l'oriente Matrix.RotateY(Ry); end;
Les faces sont orientées dans le constructor du Dé
constructor TDice.Create(Depth, Size: Integer); begin inherited Create(nil, 'cube' + IntToStr(Depth), Depth); _x := 128; _y := 128; Faces[0] := TLogoFace.Create(Self, 1, Size, 0 , 0); // la face "1" est un logo, c'est la face de référence, sans rotation Faces[1] := TDotedFace.Create(Self, 2, Size, Math.PI/2, 0); // la face "2" est à 90° sur la droite Faces[2] := TDotedFace.Create(Self, 6, Size, Math.PI , 0); // la face "6" est à 180° (de l'autre côté du dé) Faces[3] := TDotedFace.Create(Self, 5, Size,-Math.PI/2, 0); // la face "5" est à 90° sur la gauche Faces[4] := TDotedFace.Create(Self, 4, Size, 0, +Math.PI/2); // la face 4 est au dessus Faces[5] := TDotedFace.Create(Self, 3, Size, 0, -Math.PI/2); // la face 3 en dessous end;
avec ces matrices de transformation spécifiques à chaque face, il est possible de créer des fonctions moveTo et lineTo qui dessinent, en 2D, dans le plan 3D de la face
procedure TFace.lineTo2d(x, y: Double); begin Matrix.Transform2d(x, y); // projection du point 3D (x, y, 0) du plan 3D de la face vers le plan 2D de l'écran lineTo(Matrix.x, Matrix.y); // dessin effectif du trait à l'écran end;
Le problème des faces cachées
Il reste un dernier point à aborder pour terminer cet article, le problème des faces cachées. Si je dessine les 6 faces du dé dans l'ordre de leur création, j'aurais toujours la même face en avant plan alors qu'elle pourrait se trouver à l'arrière du dé.Pour éviter cela, il faut gérer "les faces cachées" et déterminer quelle face doit être au dessus des autres.
C'est un vaste sujet, que nous pourrons simplifier dans notre cas puisqu'il n'est question que d'un cube et non d'un objet complexe avec qui plus est des faces transparentes .
Notez que les cartes graphiques modernes, exploitent un principe très simple pour gérer cela avec le ZBuffer. Ce buffer est un tableau de la taille de l'image écran qui conserve pour chaque pixel dessiné sa position en Z. A chaque fois qu'on voudra dessiner un nouveau pixel, il suffira de vérifier que sa position est inférieure à celle du précédent pixel faute de quoi il n'est pas affiché.
Rien de tout cela dans Flash (version 8), il va falloir trouver une autre solution. Comme je le disais, nous avons affaire ici à un simple cube à 6 faces qui ne se chevauchent pas; on pourra se contenter de comparer la position Z du centre de chaque face, s'il est inférieur à 0 c'est que la face a tourné suffisamment pour se trouver face à l'écran, sinon elle est derrière.
procedure TFace.onEnterFrame; begin Matrix.RotateX(-Math.PI / 30); // application une rotation à chaque Frame Matrix.RotateY(+Math.PI / 60); Matrix.transform(0, 0, 0); // calculer la position du centre de la face Clear(); if Matrix.z < 0 then // si z < 0 alors la face est devant l'écran, sinon elle est à l'arrière begin swapDepths(100000 - Matrix.z * 100); // place la face à cette profondeur Render; // et la dessiner end; end;
swapDepths()
est la seule méthode que Flash nous laisse pour réordonner les clip à l'écran, on pourrait aussi garder l'ordre des clips et déterminer sur lequel doit se dessiner chaque face. Le calcule est repris d'un exemple trouvé sur le web, mais -Matrix.z
devrait tout autant faire l'affaire à la réflexion :)Conclusion
Voila, la 3D sous Flash 8 n'est pas simple, d'autant que je n'aborde pas ici la question des textures 3D. Si leur usage est possible il reste compliqué dans la mesure ou Flash ne propose pas de déformation dans l'espace.Si je trouve le temps (et/ou l'argent) pour développer FlashPascal 3 (pour Flash 10) nous pourrons développer de la 3D avec OpenGL sous Flash :)
Date de dernière modification : 14/06/2013