Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Graphics.drawTriangles : A quoi ça sert ? Comment ça marche ?

Par tlecoz (thomas le coz), le 20 septembre 2011

Hello la communauté !

Bienvenu sur mon premier tuto :)

Depuis que l'actionscript existe, il est possible de coder soi même ses propres formes vectorielles, leur appliquer un remplissage simple ou en dégradé ; bref , de reproduire par le code ce que l'on peut faire via l'IDE, en utilisant l'API de dessin. Avec l'arrivée de Flash10 il est possible de faire des animations en 3D avec une gestion de la perspective ; de nouveaux outils ont donc été ajouté à l'API dessin pour permettre de reproduire par le code les animations créées dans l'IDE. On aurait pu s'attendre à trouver une fonction Graphics.drawPlane(x,y,z,width,height,rotationX,rotationY,rotationZ) permettant de dessiner facilement un rectangle en 3D, mais non… Il n'y a que Graphics.drawTriangles qui attend des paramètres pour le moins obscure quand on a jamais fait de 3D. Je vais essayer de clarifier tout ça, voire même d'aller un peu plus loin pour montrer comment cette fonction peut être utile dans le cadre d'une animation 2D bien qu'elle ait été pensé pour faire de la 3D. Je vais être obliger d'utiliser les Matrix3D pour animer mes objets, mais ne vous inquietez pas il est beaucoup plus simple de manipuler des Matrix3D que des Matrix2D :) Au final, on posera les bases d'un moteur 3D minimaliste (mais qui suffit dans la plupart des cas). Mais commençons par le début, utilisons Graphics.drawTriangles pour dessiner…un triangle !

Dessinons un simple triangle en 2D

La fonction drawTriangle attends 4 paramètres mais un seul est obligatoire. Il s'agit d'un Vector.<Number> qui va contenir des paires de valeurs correspondant au x/y de chaque point de notre triangle. par exemple, avec flash 9 on aurait codé un triangle comme ca

 var render:Shape = new Shape();
 addChild(render);
 render.x = stage.stageWidth/2;
 render.y = stage.stageHeight/2;
 //l'utilisation des Point n'est pas necessaire ici, mais ca rend la comparaison plus lisible
 var pt1:Point = new Point(-100,-100);
 var pt2:Point = new Point(100,-100);
 var pt3:Point = new Point(0,100);
 
 render.graphics.beginFill(0xff0000);
 render.graphics.moveTo(-100,-100);
 render.graphics.lineTo(100,-100);
 render.graphics.lineTo(0,100);

Avec flash10, on pourrait le coder comme ça

 var render:Shape = new Shape();
 addChild(render);
 render.x = stage.stageWidth/2;
 render.y = stage.stageHeight/2;
 var pt1:Point = new Point(-100,-100);
 var pt2:Point = new Point(100,-100);
 var pt3:Point = new Point(0,100);
 var vertices:Vector.<Number> = new Vector.<Number>();
 vertices.push(pt1.x,pt1.y);
 vertices.push(pt2.x,pt2.y);
 vertices.push(pt3.x,pt3.y);
 
 render.graphics.beginFill(0xff0000);
 render.graphics.drawTriangles(vertices);

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu. L'intérêt est assez réduit si on veut dessiner un seul triangle, mais si on voulait dessiner 1000 triangles, il suffirait de remplir le Vector.<Number> 'vertices' avec 1000 x 3 (points) x 2 (coordonnées x/y) , c'est à dire 6000 valeurs, et flash pourrait tous les dessiner en une seule fois via Graphics.drawTriangles ce qui est beaucoup plus optimisé que 1000 'moveTo-lineTo-lineTo-lineTo'

Avant d'essayer de dessiner 1000 triangles, on a s'attarder un peu sur fonctionnement d'un unique triangle, et sur les différents paramètres attendus par la fonction drawTriangles.

Appliquer une texture sur un triangle

On a vu comment dessiner un triangle rouge ! Essayons maintenant d'appliquer une texture sur un triangle :) Un problème se pose : on dispose d'une image rectangulaire et on veut texturer un triangle (qui peut être orienté n'importe comment en fonction de la position des points).

On va donc définir de manière abstraite des zones triangulaires sur notre texture pour pouvoir l'appliquer sur notre triangle. Il ne s'agit pas ici de reproduire exactement le triangle que l'on veut dessiner au niveau de notre texture, puis de l'appliquer sur le triangle, c'est un peu plus subtil que ça :)

On donne simplement à flash , pour chaque point de notre triangle, un point de repère dans la texture. Par exemple, le premier point correspondra au coin haut-gauche de notre image, le deuxième correspondra au coin haut-droit et le dernier au coin bas-gauche. A partir de ces indications, quelles que soient les positions des différents points, flash sera en mesure de texturer notre triangle en s'occupant tout seul des déformations à appliquer (si nécessaire). Par exemple, si les deux premiers points de mon triangles n'étaient pas alignés, flash “rotationnerait” la texture tout seul

Evidemment, on ne peut pas dire à flash “ce point correspond au coin haut-gauche de l'image” de manière aussi explicite ; on va devoir creer un nouveau Vector.<Number> qui fonctionnera un peu comment notre tableau 'vertices' à ceci près qu'il gèrera des triplets de coordonnées x,y,z car le texturing a été pensé dans le cadre d'un utilisation 3D. Contrairement aux coordonnées contenues dans 'vertices' permettant de dessiner réellement le triangle, les coordonnées liées à la textures sont relatives (!= absolue) et se traduisent par des valeurs comprises entre 0 et 1;

par exemple, un tableau contenant les repères de texturing pourrait ressembler à ça

var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0) //  x = 0 --> 0% de la largeur de l'image, c'est a dire le bord gauche
                 //  y = 0 --> 0% de la hauteur de l'image, c'est a dire le bord haut
				 //  z = 0 --> quand on créé les reperes de textures, z est toujours egale à 0
				 //          cette valeur sera utilisé plus tard en interne, mais on ne s'en soucie pas
 
uvts.push(1,0,0) // x = 100% de l'image , c'est a dire le bord droit
                 //  y=0% de l'image, c'est a dire le bord haut
				 //  z=0 
 
uvts.push(0,1,0) // x = 0% de la largeur de l'image , c'est a dire le bord gauche
                 // y = 100% de la hauteur de l'image, c'est a dire le bord bas
				 // z=0

Comme je l'ai dit plus haut, le texturing à été pensé en 3D, on ne peut donc pas texturer directement notre triangle composé de points en 2D avec notre texture disposant de repères 3D.

On va devoir traduire notre triangle en 3D. Pour faire cela, on va s'y prendre de la même manière que notre tableau 'vertices' en mettant à la suite des coordonnées des points x,y et z (puisqu'on est en 3D) pour chacun des points.

ce qui nous donne

 var vertices3D:Vector.<Number> = new Vector.<Number>();
 vertices3D.push(pt1.x,pt1.y,0);
 vertices3D.push(pt2.x,pt2.y,0);
 vertices3D.push(pt3.x,pt3.y,0);

Le premier paramètre attendu par la méthode Graphics.drawTriangles est un Vector.<Number> contenant une suite de coordonnées en 2 dimension (x/y uniquement) , on peut donc en déduire qu'on ne peut pas utiliser ce tableau directement pour dessiner notre triangle.

On dispose d'un tableau de coordonnées 3D, d'un tableau de répère de texture 3D et on aimerait obtenir un tableau de coordonnée 2D avec les repères de textures adapté à l'affichage 2D. Ca tombe bien, c'est exactement le role de la fonction Utils3D.projectVectors :)

La fonction Utils3D.projectVectors attend 4 paramètres - une matrice3D –> pour l'instant, on ne s'en soucie pas, donc on se contentera de creer un new Matrix3D() - un Vector.<Number> contenant des coordonnées de points en 3D - un Vector.<Number> qui contiendra, à la fin de la fonction, les coordonnées des points en 2D - un Vector.<Number> contenant les repères de textures.

//---> les coordonnées liées aux textures sont modifié en place ; c'est à dire qu'on conserve un systeme 3D mais qui est interpolé pour s'afficher dans un univers 2D. 

Voila le code permettant de texturer un triangle

 
import flash.display.BitmapData;
import flash.geom.Utils3D;
 
var render:Shape = new Shape();
render.x = stage.stageWidth/2;
render.y = stage.stageHeight/2;
addChild(render);
 
var pt1:Point = new Point(-100,-100);
var pt2:Point = new Point(100,-100);
var pt3:Point = new Point(0,100);
 
var vertices3D:Vector.<Number> = new Vector.<Number>();
vertices3D.push(pt1.x,pt1.y,0);
vertices3D.push(pt2.x,pt2.y,0);
vertices3D.push(pt3.x,pt3.y,0);
 
var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0);
uvts.push(1,0,0);
uvts.push(0,1,0);
 
var vertices2D:Vector.<Number> = new Vector.<Number>();
 
trace("avant le passage d'Utils3D.projectVectors---------")
trace("vertices2D = "+ vertices2D);  //--> vertices2D = 
trace("uvts = "+uvts); // -> uvts = 0,0,0,1,0,0,0,1,0
trace("--------------------------------------------------");
 
 
Utils3D.projectVectors(new Matrix3D(),vertices3D,vertices2D,uvts);
 
trace("après le passage d'Utils3D.projectVectors---------")
trace("vertices2D = "+ vertices2D); // -> vertices2D = -100,-100,100,-100,0,100
trace("uvts = "+uvts); // -> uvts = 0,0,1,1,0,1,0,1,1
trace("--------------------------------------------------");
 
var monImage:BitmapData = new Tof(0,0) as BitmapData;
 
render.graphics.beginBitmapFill(monImage,null,false,true);
render.graphics.drawTriangles(vertices2D,null,uvts);
//je vais expliquer très bientot à quoi correspond le deuxieme parametre de la fonction Graphics.drawTriangle, mais on ne s'embarasse pas avec ça pour le moment

On peut voir que Utils3d.projectVectors à rempli notre tableau Vertice2D qui était vide, mais aussi qu'il a modifié les valeurs de notre tableau 'uvts'.

//AVANT Utils3d.projectVectors -> uvts = 0,0,0,1,0,0,0,1,0
//APRES Utils3d.projectVectors -> uvts = 0,0,1,1,0,1,0,1,1

On peut observer des différence au niveau de la 3eme, 6eme et 9eme valeurs ; Autrement dit la position Z de chaque point de repère de texture a été modifié (puisque le tableau uvts contient des triplets de valeurs XYZ).

A quoi correspond cette modification ?

Je ne sais pas exactement comment fonctionne Utils3d.projectVectors en interne, mais elle normalise la distance de chaque point et stocke le resultat au niveau du z de chaque coordonnée de texture. Par “normaliser”, je veux dire que le “z” de chaque point est divisé par le “z” du point le plus éloigné afin d'obtenir des valeurs comprises entre 0 et 1 pour chaque z (dans le tableau uvts).

Ces explications n'ont pas grand intérêt pour le moment, mais on en aura besoin par la suite (ne vous inquietez pas, ce ne sera pas bien méchant :) )

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Faire bouger un triangle en 3D

Maintenant qu'on sait dessiner et texturer le triangle le plus optimisé que l'on puisse faire en AS3, on va apprendre à le faire bouger parce que dejà qu'on a rarement besoin d'un unique triangle, si en plus il ne bouge pas….

Lorsque l'on fait une animation classique en 2D avec l'API de dessin pour dessiner un trait en mouvement par exemple, il est nécessaire à chaque frame d'effacer le dessin et de tout redéssiner. Cela fonctionne de la même manière dans le cas d'une animation reposant sur Graphics.drawTriangles.

Cela sous entend que l'on doit trouver un moyen de modifier, à chaque frame, les coordoonnées de tout nos points , directement depuis le Vector.<Number> 'vertices' . Dans le cas d'un triangle unique, on pourrait à la limite, modifier chaque coordonnées une a une et prier pour une pas faire une erreur de calcul ; mais dans le cas d'une forme complexe composé de milliers de triangles, c'est exclu .

Fort heureusement, la classe Matrix3D nous permet d'appliquer une transformation sur un tableau de points grace à la méthode Matrix3D.transformVectors.

Ne partez pas ! C'est vraiment simple à utiliser, en fait c'est même beaucoup plus simple à utiliser que les opérations que l'on fait d'habitude avec les Matrix car en 3D, chaque objet dispose d'un repère centré (les coordonnées 0,0,0 correspondent au centre de l'objet) contrairement au DisplayObject dont l'origine se trouve généralement en haut à gauche, ce qui complexifie souvent les calculs.

Comme dit plus haut la méthode Matrix3D.transformVectors applique une transformation sur un ensemble de points, mais avant il faut parametrer la matrice pour lui dire comment les transformer. Pour cela, pas besoin d'être bon en math, il suffit d'utiliser des méthodes de la classe Matrix3D qui vont faire le boulot à notre place :)

- pour positionner un objet3D, on utilise la méthode Matrix3D.appendTranslation(X,Y,Z)

dans laquelle X,Y et Z correspondent aux coordonnées voulues

- pour modifier l'echelle de l'objet, on utilise la méthode Matrix3D.appendScale(scaleX,scaleY,scaleZ)

dans laquelle scaleX,scaleY et scaleZ correspondent aux echelles voulues  

- pour modifier les rotations, on utilise la méthode Matrix3D.appendRotation(angleEnDegrés,axeDeRotation,pivot)

dans laquelle - angleEnDedrée contient l'angle de la rotation a effectuer en degrés
              - axeDeRotation l'une des variables static suivantes Vector3D.X_AXIS,Vector3D.Y_AXIS et Vector3D.Z_AXIS correspondant aux différent axes de rotations
			- pivot (null par défaut) contient un Vector3D qui définit, comme son nom l'indique, le pivot par rapport auquel l'objet tourne (par défaut, l'objet tourne par rapport à lui même)
			

Comme avec les Matrix2D, l'ordre des instructions que l'on applique à la Matrix3D à son importance.

Par exemple, si je fais

var m:Matrix3D = new Matrix3D();
m.appendRotation(45,Vector3D.X_AXIS);
m.appendTranslation(0,0,500);

je demande à la matrix d'incliner l'objet à 45°, puis de le déplacer tout en étant incliné de 500px en profondeur. L'objet (si je lui appliquais cette matrix) devrait apparaitre devant nous, un peu loin (à 500px de nous pour être exact) et incliné à 45.

Si j'inversais les deux instructions comme ça

var m:Matrix3D = new Matrix3D();
m.appendTranslation(0,0,500);
m.appendRotation(45,Vector3D.X_AXIS);

Je demandes d'abord à l'objet de se deplacer de 500px en profondeur par rapport a son point d'origine (0,0,0), puis je rotationne sa position de 45°. Mais le problème c'est que la rotation se fait ici par rapport au point d'origine et pas par rapport à l'objet. L'objet (si on lui applique cette matrix) va donc être positionné comme si il était placé le long d'un cercle (de 500px de rayon, et à un angle de 45° par rapport au centre de ce cercle).

Pour ne pas avoir de surprise, le plus simple est d'appliquer le déplacement des XYZ (appendTranslation) en dernier, de telle manière que les rotations et la mise à l'echelle soient bien effectué par rapport au centre de l'objet.

Pour gérer une transformation 3D comme le fait Flash, on peut donc faire

var x:Number = 100;
var y:Number = 200;
var z:Number = 300;
var scaleX:Number = 1;
var scaleY:Number = 1;
var scaleZ:Number = 1;
var rotationX:Number = 15;
var rotationY:Number = 30;
var rotationZ:Number = 45;
 
var m:Matrix3D = new Matrix3D();
m.appendScale(scaleX,scaleY,scaleZ);
m.appendRotation(rotationX,Vector3D.X_AXIS);
m.appendRotation(rotationY,Vector3D.Y_AXIS);
m.appendRotation(rotationZ,Vector3D.Z_AXIS);
m.appendTranslation(x,y,z);

Simple non ?

Vient enfin le moment ou il faut l'appliquer à notre tableau de points :D

On arrive donc à Matrix3D.transformVectors dont je parlais plus haut, cette méthode attend attend 2 parametres : - le premier correpond au tableau de points avant la transformation - le deuxieme correspond a un tableau qui va accueillir les ccordonnées des points aprés la transformation matricielle

on va donc creer un nouveau Vector.<Number> contenant autant d'entrée qu'il y en a dans 'vertices' pour stocker les coordonnées de nos points transformés par la matrice.

Pour ne pas se prendre la tête à faire une boucle, on va cloner le Vector.<Number> grace à Vector.concat()

var transform3D:vector.<Number> = vertices.concat();

Voila le code permettant de faire tourner notre triangle sur lui meme, en agissant sur les rotationX,rotationY,rotationZ

 
var render:Shape = new Shape();
render.x = stage.stageWidth/2
render.y = stage.stageHeight / 2;
addChild(render);
 
var pt1:Point = new Point(-100,-100);
var pt2:Point = new Point(100,-100);
var pt3:Point = new Point(0,100);
 
var vertices3D:Vector.<Number> = new Vector.<Number>();
vertices3D.push(pt1.x,pt1.y,0);
vertices3D.push(pt2.x,pt2.y,0);
vertices3D.push(pt3.x,pt3.y,0);
 
var transform3D:Vector.<Number> = vertices3D.concat();
 
var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0);
uvts.push(1,0,0);
uvts.push(0,1,0);
 
var vertices2D:Vector.<Number> = new Vector.<Number>();
 
 
var monImage:BitmapData = new Tof(0,0) as BitmapData;
 
var matrix:Matrix3D = new Matrix3D();
 
addEventListener(Event.ENTER_FRAME,update);
 
 
function update(e:Event):void{
    matrix.appendRotation(1,Vector3D.X_AXIS);
    matrix.appendRotation(1,Vector3D.Y_AXIS);
    matrix.appendRotation(1,Vector3D.Z_AXIS);
    matrix.transformVectors(vertices3D,transform3D);
 
    Utils3D.projectVectors(new Matrix3D(),transform3D,vertices2D,uvts);
 
    render.graphics.clear()
    render.graphics.beginBitmapFill(monImage,null,false,true);
    render.graphics.drawTriangles(vertices2D,null,uvts);
}

Ce qu'il est important de comprendre ici, c'est que le tableau vertices3D n'est jamais modifié. L'objet Matrix3D se base sur notre tableau 'vertice3D' pour remplir le tableau 'transform3D' avec les coordonnées tranformées.

Il peut être utile dans certains cas d'appliquer la transformation directement sur les vertices3D si on veut modifier la position d'un objet de manière permanente mais j'y reviendrais quand on animera plusieurs objets en même temps :)

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Choisir de dessiner le recto, le verso, ou le recto-verso d'un triangle

maintenant qu'on a un triangle en mouvement, on peut s'interesser au dernier paramètre de la fonction Graphics.drawTriangle

Ce paramètre “culling” permet de définir si l'on veut texturer le recto du triangle, ou le verso, ou le recto/verso. Par défaut, la propriété culling est défini sur “none” ce qui correspond à texturer le recto et le verso. Si on essaye de modifier la valeur du culling par “positive” ou “negative”, le triangle sera texturer ou pas en fonction de son orientation par rapport à la caméra.

La propriété “culling” peut être utilisée - soit pour économiser des ressources, dans le cas ou l'objet 3D fait toujour face à la camera et qu'il n'est pas necessaire de dessiner le verso. - soit pour avoir 1 texture pour le recto et 1 autre pour le verso

Par défaut, le culling est défini sur “none”, autrement dit on dessine le triangle quoi qu'il arrive. On peut modifier cette valeur par “negative” ou “positive” pour choisir de dessiner qui sont ou ne sont pas en face de nous.

Pour déterminer si le triangle est en face de nous ou pas, Flash se base sur la construction de notre tableau 'indices' … et c'est dommage car je n'en ai pas encore parlé (le deuxieme parametre de la fonction drawTriangle, pour l'instant défini sur null). J'y reviendrais quand on aura construit le rectangle3D le plus optimisé qu'on puisse faire en AS3, c'est à dire bientôt :)

Voila le code d'un triangle doté de 2 textures

 
function update(e:Event):void{
    matrix.appendRotation(1,Vector3D.X_AXIS);
    matrix.appendRotation(1,Vector3D.Y_AXIS);
    matrix.appendRotation(1,Vector3D.Z_AXIS);
    matrix.transformVectors(vertices3D,transform3D);
 
    Utils3D.projectVectors(new Matrix3D(),transform3D,vertices2D,uvts);
 
    render.graphics.clear()
	//on dessine le recto
    render.graphics.beginBitmapFill(recto,null,false,true);
    render.graphics.drawTriangles(vertices2D,null,uvts,"negative");
	//puis le verso
	render.graphics.beginBitmapFill(verso,null,false,true);
    render.graphics.drawTriangles(vertices2D,null,uvts,"positive");
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

création d'un objet3D composé de plusieurs triangles : un rectangle3D

Maintenant qu'on a ,à peu près, fait le tour de ce qu'on peut faire avec un triangle unique, on va essayer de voir comment créer un objet 3D composé de plusieurs triangles, par exemple, au hasard, une plane (c'est à dire un rectangle en 3D). N'importe quel rectangle peut être décomposé en 2 triangles rectangles, il nous suffit donc de creer deux triangles pour créer implicitement un rectangle.

Puisqu'on sait construire un triangle en 3D, qu'on sait qu'un rectangle est composé de deux triangles, que chaque triangle contient 3 points, on a de prime abord tendance à créer le double de coordonnées dans les tableaux 'vertices' et 'uvts'. Quelque chose comme ca :

 
import flash.geom.Point;
 
var render:Shape = new Shape();
render.x = stage.stageWidth/2
render.y = stage.stageHeight / 2;
addChild(render);
 
var pt0:Point = new Point(-100,-100);
var pt1:Point = new Point(100,-100);
var pt2:Point = new Point(-100,100);
var pt3:Point = new Point(100,100);
 
var vertices3D:Vector.<Number> = new Vector.<Number>();
//1er triangle
vertices3D.push(pt0.x,pt0.y,0);
vertices3D.push(pt1.x,pt1.y,0);
vertices3D.push(pt2.x,pt2.y,0);
//2eme triangle
vertices3D.push(pt1.x,pt1.y,0);
vertices3D.push(pt3.x,pt3.y,0);
vertices3D.push(pt2.x,pt2.y,0);
 
var transform3D:Vector.<Number> = vertices3D.concat();
 
var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0);
uvts.push(1,0,0);
uvts.push(0,1,0);
 
uvts.push(1,0,0);
uvts.push(1,1,0);
uvts.push(0,1,0);
 
var vertices2D:Vector.<Number> = new Vector.<Number>();
 
 
var recto:BitmapData = new Tof(0,0) as BitmapData;
var verso:BitmapData = new Tof2(0,0) as BitmapData;
 
var matrix:Matrix3D = new Matrix3D();
 
addEventListener(Event.ENTER_FRAME,update);
 
 
function update(e:Event):void{
    matrix.appendRotation(1,Vector3D.X_AXIS);
    matrix.appendRotation(1,Vector3D.Y_AXIS);
    matrix.appendRotation(1,Vector3D.Z_AXIS);
    matrix.transformVectors(vertices3D,transform3D);
 
    Utils3D.projectVectors(new Matrix3D(),transform3D,vertices2D,uvts);
 
    render.graphics.clear()
	//on dessine le recto
    render.graphics.beginBitmapFill(recto,null,false,true);
    render.graphics.drawTriangles(vertices2D,null,uvts,"negative");
	//puis le verso
	render.graphics.beginBitmapFill(verso,null,false,true);
    render.graphics.drawTriangles(vertices2D,null,uvts,"positive");
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Cela fonctionne, notre plane est bien affiché à l'écran. Cependant, même si cette approche est correcte, ce n'est pas la manière la plus optimisée de créer un rectangle en 3D.

création d'un objet3D en utilisant le strict minimum de points

En effet, un rectangle étant doté de 4 points, cela paraît un peu étrange de devoir en faire bouger 6 (2 x 3) pour obtenir un résultat en 3D. L'idéal serait de faire en sorte qu'un point puisse être utilisé par plusieurs triangle

Actuellement on a une structure comme ca

A-------B  D
|     /  / |
|    /  /  |
|   /  /   |
|  /  /    |
| /  /     |
|/  /      |
C  F-------E

alors qu'idéalement, il faudrait avoir ça

A-------B 
|     / |
|    /  |
|   /   |
|  /    |
| /     |
|/      |
C-------D

Pour que flash arrive à comprendre comment convertir 4 points en 2 triangles, il faut lui expliquer comment sont agencé les triangles.

par exemple, si on a

 
var pt1:Point = new Point(-100,-100);
var pt2:Point = new Point(100,-100);
var pt3Point = new Point(-100,100);
var pt4:Point = new Point(100,100);
 
var verts3D:Vector.<Number> = new Vector.<Number>();
verts.push(pt1.x,pt1.y,0);
verts.push(pt2.x,pt2.y,0);
verts.push(pt3.x,pt3.y,0);
verts.push(pt4.x,pt4.y,0);

Flash interprete ce tableau par triplet de valeur. Ce qui fait que d'une certaine maniere, ce tableau ne contient pas vraiment 12 valeurs mais plutot 4×3.

Pour prevenir Flash que ces 4 points décrivent 2 triangles, il faut que j'explique à flash quelles valeurs il doit prendre dans mon tableau 'verts' pour représenter chacun de mes points. Pour faire cela, je vais devoir créer un Vector.<int> qui va contenir des triplets de valeurs, représentant pour chacune l'index du points dans le tableau “verts”

Par exemple, pour lui dire que les 3 premiers points forment un triangle, il faudrait créer un Vector.<int> de ce type :

 
	var indices:Vector.<int> = new Vector.<int>();
	indices.push(0,1,2);
 
	//0 fait ici référence au premier triplet de coordonnées contenues dans "verts3D", c'est a dire au premier point   //--> verts.push(pt1.x,pt1.y,0); 
	//1 fait ici référence au deuxieme triplet de coordonnées contenues dans "verts3D", c'est a dire au deuxieme point //--> verts.push(pt2.x,pt2.y,0);
    //2 fait ici référence au trosieme triplet de coordonnées contenues dans "verts3D", c'est a dire au troisieme point //-> verts.push(pt3.x,pt3.y,0);

pour décrire la structure du deuxieme triangle, il suffit d'ajouter

    indices.push(1,3,2);
L'ordre dans lequel les points décrivent le triangle est important.
Dans mon exemple ci dessus les triangles sont décrit par les points 0,1,2 et 1,3,2. 
Si j'ai décrit le deuxième avec les point 1,3,2 plutot que 1,2,3 c'est pour faire en sorte que les points soient placé dans le sens des aiguille d'une montre.

Cela à son importance si on utilise la propriété 'culling' car le sens de placement des points détermine s'il s'agit du recto ou du verso. De fait, si on décrit les triangles sans se préoccuper du sens des points et qu'on utilise le culling, on va se retrouver avec triangles qui apparaissent quand il ne faut pas et d'autre qui n'apparaissent pas quand il faut.


Voila a quoi ressemble le code d'une Plane optimisé

<code actionscript>
	import flash.geom.Point;
var render:Shape = new Shape();
render.x = stage.stageWidth/2
render.y = stage.stageHeight / 2;
addChild(render);
 
var pt0:Point = new Point(-100,-100);
var pt1:Point = new Point(100,-100);
var pt2:Point = new Point(-100,100);
var pt3:Point = new Point(100,100);
 
var vertices3D:Vector.<Number> = new Vector.<Number>();
vertices3D.push(pt0.x,pt0.y,0);
vertices3D.push(pt1.x,pt1.y,0);
vertices3D.push(pt2.x,pt2.y,0);
vertices3D.push(pt3.x,pt3.y,0);
var transform3D:Vector.<Number> = vertices3D.concat();
 
var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0);
uvts.push(1,0,0);
uvts.push(0,1,0);
uvts.push(1,1,0); 
var indices:Vector.<int> = new Vector.<int>();
indices.push(0,1,2);
indices.push(1,3,2);
var vertices2D:Vector.<Number> = new Vector.<Number>();
var recto:BitmapData = new Tof(0,0) as BitmapData;
var verso:BitmapData = new Tof2(0,0) as BitmapData;
var matrix:Matrix3D = new Matrix3D();
 
addEventListener(Event.ENTER_FRAME,update);
 
function update(e:Event):void{
	matrix.appendRotation(1,Vector3D.X_AXIS);
	matrix.appendRotation(1,Vector3D.Y_AXIS);
	matrix.appendRotation(1,Vector3D.Z_AXIS);
	matrix.transformVectors(vertices3D,transform3D);
 
	Utils3D.projectVectors(new Matrix3D(),transform3D,vertices2D,uvts);
 
	render.graphics.clear()
	//on dessine le recto
	render.graphics.beginBitmapFill(recto,null,false,true);
	render.graphics.drawTriangles(vertices2D,indices,uvts,"negative");
	//puis le verso
	render.graphics.beginBitmapFill(verso,null,false,true);
	render.graphics.drawTriangles(vertices2D,indices,uvts,"positive");
}

</code>

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

création d'un objet3D composé de plusieurs centaines de triangles

Maintenant qu'on sait créer une forme composé de plusieurs triangles, on va se dégourdir un peu les doigts en dessinant une forme composé de plusieurs centaines de triangles :) On va commencer par une structure simple : un cylindre.

Pour trouver l'emplacement d'un point le long d'un cercle en 2D, on utilise les sinus et les cosinus. Et bien on va faire la même chose à ceci prés que le calcul du sinus ne correspondra pas à une position sur l'axe des Y mais sur l'axe des Z , afin d'obtenir un cercle qui parte en profondeur plutot qu'un cercle vu de haut.

le code

import flash.display.BitmapData;
import flash.geom.Matrix3D;
 
var radius:Number = 250; //le rayon du cylindre
var nbSegment:int = 100; //le nombre de segment (un segment = une plane en qq sorte, donc 2 triangles)
var ringHeight:Number = 100; //la hauteur du cylindre
var halfRingHeight:Number = ringHeight/2; //la moitié de la hauteur du cylindre
 
//l'idée est de créer une sorte de ruban texturé qu'on applique le long d'un cercle
//le ruban est structuré comme ça au niveau du tableau indices
//
//  0--2--4--6--...
//  | /| /| /| /|
//  |/ |/ |/ |/ |
//  1--3--5--7--...
//
//Si on regarde bien , on peut voir que sauf pour les 2 premiers points
//chaque fois qu'on ajoute une paire de point, deux triangles seront créés.
//Ce qui sous entend qu'on peut facilement créer tout nos triangles via une boucle :D
//
//puisqu'on a plein de triangle, on a plein de coordonnée de textures à trouver.
//
// pour les coordonnées Y c'est facile, on veut que le haut de l'image corresponde au haut du
//cylindre, et le bas de l'image au bas du cylindre.
//
//Dans le schema au dessus, les point 0,2,4,6 représente le haut du bandeau, la coordonnée y des
//repère de texture sera donc 0 pour ces points. Reciproquement, la coordonnées y des repères 
//de texture correspondant aux points 1,3,5,7 sera 1 (100% de la hauteur de l'image)
//
//pour récupérer les coordonnées de textures sur l'axe des X, il suffit de "décomposer" la texture
//en autant de bandelette qu'on a de segment sur notre cylindre, et d'indiquer la position x de chaque 
//bandelette sous forme de pourcentage de la largeur de l'image
//
//dans notre cas, on a 100 segment, donc le pourcentage correspondant a 1 segment est :
//ratio = 1 / nbSegment, donc 1/100, donc 0.01
//
//si je l'applique au schema ci dessus, cela donnerait
//
//  (x:0,y:0,z:0)----(x:0.01,y:0,z:0)----(x:0.02,y:0,z:0)--(x:0.03,y:0,z:0)----...
//  |                       |                   |                 |
//  |                       |                   |                 |
//  (x:0,y:1,z:0)----(x:0.01,y:1,z:0)----(x:0.02,y:1,z:0)--(x:0.03,y:1,z:0)----...
//
 
 
//je calcul le ratio me permettant de calculer les coordonnées X des repères de texture.
var uvx:Number = 1/(nbSegment);
 
//comme d'hab, je créé un tableau contenant mes points en 3D
var verts3D:Vector.<Number> = new Vector.<Number>();
 
 
//je créé d'abord un premier segment, histoire de mettre en place la structure
//ensuite, chaque passage dans ma boucle correspondra à créer un autre segment
 
 
//d'abord les 2 premiers points (0 et 1 sur mon schema)
verts3D.push(radius,-halfRingHeight,0); //0
verts3D.push(radius,halfRingHeight,0);  //1
 
//je cherche l'angle du prochain segment
var a:Number = (Math.PI*2) / (nbSegment);
 
//je positionne les 2 autres points me permettant de créer 2 triangles, et qui serviront
//de points de 2 départ à deux futur triangles.
 
verts3D.push(Math.cos(a)*radius,-halfRingHeight,Math.sin(a)*radius);
verts3D.push(Math.cos(a)*radius,halfRingHeight,Math.sin(a)*radius);
 
//puisqu'on a assez de point pour décrire des triangles, 
//on peut commencer à remplir le tableau uvts et le tableau indices
var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0);
uvts.push(0,1,0);
uvts.push(uvx,0,0);
uvts.push(uvx,1,0);
 
var verts2D:Vector.<Number>;
 
var indices:Vector.<int> = new Vector.<int>();
indices.push(0,1,2);
indices.push(1,3,2);
 
//on crée une variable qui stocke l'index actuel à utiliser dans notre tableau indice
//on vient de créer 4 points, donc l'index = 4
var index:int = 4;
 
 
var i:int;
var rx:Number;
 
//et maintenant que toute notre structure est posé, on peut traiter les autres points
//à la volée
 
for(i=2;i<nbSegment;i++){
	a = (Math.PI*2) * (i/(nbSegment-1));
	rx = uvx * (i-1);
	verts3D.push(Math.cos(a)*radius,-halfRingHeight,Math.sin(a)*radius);
	verts3D.push(Math.cos(a)*radius, halfRingHeight,Math.sin(a)*radius);
 
	uvts.push(rx,0,0);
	uvts.push(rx,1,0);
 
	indices.push(int(index - 1),index,int(index - 2));
	indices.push(index-1,int(index+1),int(index));
 
	index +=2
}
 
//la taille de tout ces tableaux ne changera jamais,
//et ils seront lu en permanence dans un enterFrame,
//on fixe leur taille pour optimiser les performances
 
verts3D.fixed = true;
uvts.fixed = true;
indices.fixed = true;
verts2D = new Vector.<Number>((verts3D.length / 3) * 2 ,true);
 
var transform3D:Vector.<Number> = verts3D.concat();
transform3D.fixed = true;
 
//ensuite on fait comme d'habitude :)
 
var render:Shape = new Shape();
render.x = stage.stageWidth/2;
render.y = stage.stageHeight/2;
addChild(render);
 
var image1:BitmapData = new Tof(0,0) as BitmapData;
var image2:BitmapData = new Tof2(0,0) as BitmapData;
 
var matrix:Matrix3D = new Matrix3D();
var contenerMatrix:Matrix3D = new Matrix3D();
stage.addEventListener(MouseEvent.MOUSE_MOVE,update);
function update(e:MouseEvent):void{
 
 
	matrix.identity();
	matrix.appendRotation(render.mouseY/render.y * 180,Vector3D.X_AXIS);
	matrix.appendRotation(render.mouseX/render.x * 180,Vector3D.Y_AXIS);
	matrix.transformVectors(verts3D,transform3D);
 
	matrix.identity();
	Utils3D.projectVectors(matrix,transform3D,verts2D,uvts);
 
	render.graphics.clear()
	//render.graphics.lineStyle(0);
	render.graphics.beginBitmapFill(image1,null,false,false);
	render.graphics.drawTriangles(verts2D,indices,uvts,"negative");
 
	render.graphics.beginBitmapFill(image2,null,false,false);
	render.graphics.drawTriangles(verts2D,indices,uvts,"positive");
 
	e.updateAfterEvent();
 
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

On peut remarquer la structure ne change pas beaucoup en fonction de si on veut afficher un triangle ou une centaine de triangle. Une fois l'objet3D créé (les vertices, les indices, les repère de texture) , le reste de la structure ne change pas et le code commence à devenir rentable (parce que 70 lignes pour dessiner un carré, c'est bof, mais 100 lignes pour dessiner un cylindre texturé, c'est pas si mal :) )

Cependant, on a un peu triché car l'objet que l'on vient de dessiner est une forme convexe, ce qui simplifie un peu le traitement car le culling permet de determiner si le triangle est visible ou pas, et puisqu'aucun des triangles qui sont face à la camera ne se superposent du fait de la forme convexe de l'objet, il n'y a pas de problème de triangle situé plus loin qui apparaissent devant d'autres plus près, bref des problèmes de z-sorting.

Je vais en parler juste après, mais d'abord, j'aimerais introduire la perspective :)

Ajout de la perspective

On a un cylindre texturé qui bouge en 3D, c'est chouette mais si on regarde bien, on peut voir qu'il ne se comporte pas de manière réaliste. En effet, le centre du cylindre devrait être plus grand que les extremité car il est proche de nous. Les extremité sont à une distance équivalent au rayon du cylindre et devraient donc apparaitre légèrement plus petit.

Actuellement si on est en face du cylindre , on voit ca

et on devrait voir ca

Je ne vais pas trop m'attarder sur comment fonctionne une perspective (pour être sur de ne pas me tromper, et parce que tout est expliqué dans l'aide de Flash au niveau de la classe PerspectiveProjection), je vais plutôt vous montrer comment l'utiliser (ce sera plus simple, et plus court :) )

Pour créer une perspective, on utiliser l'objet PerspectiveProjection. Cet objet va nous permettre de générer une matrix3D décrivant la transformation-qui-va-bien pour créer une perspective.

Je ne saurais pas expliquer pourquoi (c'est d'ailleurs pour ça que je ne rentre pas dans les details de PerspectiveProjection ^^) mais le fait d'appliquer une perspective modifie la position des points sur l'axe des Z (c'est comme si la caméra était plaqué contre l'objet 3D) , il est donc nécessaire de repositionner la “camera” grace à une distance fournie par l'objet PerspectiveProjection.

var pers:PerspectiveProjection = new PerspectiveProjection();
var distance:Number = pers.focalLength //--> la distance à laquelle on doit se positionner 
                                       //par rapport à l'objet pour retrouver le positionnement voulue
var persMatrix:Matrix3D = pers.toMatrix3D();

je ne peux pas faire un Matrix3D.appendTranslation directement sur persMatrix pour corriger la distance, car les opérations doivent être faite dans le bon ordre. Je me rappelle avoir dit qu'il fallait utiliser Matrix.appendTranslation en dernier, mais pour la gestion de la perspective c'est différent : on doit corriger la distance avant d'appliquer la perspective.

Pour faire cela, on va créer une nouvelle Matrix3D sur laquelle on va faire le appendTranslation, et on la multipliera ensuite par la matrice contenant la perspective et la matrice obtenu corrigera la distance puis appliquera la perspective. Pour multiplier une matrix3d par une autre matrix3d, c'est aussi simple que d'appeler Matrix3D.append(monAutreMatrix3D).

var worldMatrix:Matrix3D = new Matrix3D();
worldMatrix.appendTranslation(0,0,pers.focalLength + radius) // j'ajoute le radius car notre objet est créé en profondeur
worldMatrix.append(persMatrix);

ensuite, pour appliquer notre perspective, c'est tout simple, il suffit de remplacer

matrix.identity();
Utils3D.projectVectors(matrix,transform3D,verts2D,uvts);

par

Utils3D.projectVectors(worldMatrix,transform3D,verts2D,uvts);

Et paf ! Ca marche !

Voila le code complet du cylindre avec la perspective

 
import flash.display.BitmapData;
import flash.geom.Matrix3D;
 
var radius:Number = 250; //le rayon du cylindre
var nbSegment:int = 100; //le nombre de segment (un segment = une plane en qq sorte, donc 2 triangles)
var ringHeight:Number = 100; //la hauteur du cylindre
var halfRingHeight:Number = ringHeight/2; //la moitié de la hauteur du cylindre
 
var uvx:Number = 1/(nbSegment);
var verts3D:Vector.<Number> = new Vector.<Number>();
verts3D.push(radius,-halfRingHeight,0); //0
verts3D.push(radius,halfRingHeight,0);  //1
 
var a:Number = (Math.PI*2) / (nbSegment);
 
verts3D.push(Math.cos(a)*radius,-halfRingHeight,Math.sin(a)*radius);
verts3D.push(Math.cos(a)*radius,halfRingHeight,Math.sin(a)*radius);
 
var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0);
uvts.push(0,1,0);
uvts.push(uvx,0,0);
uvts.push(uvx,1,0);
 
var verts2D:Vector.<Number>;
 
var indices:Vector.<int> = new Vector.<int>();
indices.push(0,1,2);
indices.push(1,3,2);
 
var index:int = 4;
 
 
var i:int;
var rx:Number;
 
for(i=2;i<nbSegment;i++){
	a = (Math.PI*2) * (i/(nbSegment-1));
	rx = uvx * (i-1);
	verts3D.push(Math.cos(a)*radius,-halfRingHeight,Math.sin(a)*radius);
	verts3D.push(Math.cos(a)*radius, halfRingHeight,Math.sin(a)*radius);
 
	uvts.push(rx,0,0);
	uvts.push(rx,1,0);
 
	indices.push(int(index - 1),index,int(index - 2));
	indices.push(index-1,int(index+1),int(index));
 
	index +=2
}
 
verts3D.fixed = true;
uvts.fixed = true;
indices.fixed = true;
verts2D = new Vector.<Number>((verts3D.length / 3) * 2 ,true);
 
var transform3D:Vector.<Number> = verts3D.concat();
transform3D.fixed = true;
 
var render:Shape = new Shape();
render.x = stage.stageWidth/2;
render.y = stage.stageHeight/2;
addChild(render);
 
var image1:BitmapData = new Tof(0,0) as BitmapData;
var image2:BitmapData = new Tof2(0,0) as BitmapData;
 
var matrix:Matrix3D = new Matrix3D();
var contenerMatrix:Matrix3D = new Matrix3D();
stage.addEventListener(MouseEvent.MOUSE_MOVE,update);
 
 
var worldMatrix:Matrix3D = new Matrix3D();
var pers:PerspectiveProjection = new PerspectiveProjection();
var persMatrix:Matrix3D = pers.toMatrix3D();
 
worldMatrix.appendTranslation(0,0,pers.focalLength + radius);
worldMatrix.append(persMatrix);
 
function update(e:MouseEvent):void{
 
	matrix.identity();
	matrix.appendRotation(render.mouseY/render.y * 180,Vector3D.X_AXIS);
	matrix.appendRotation(render.mouseX/render.x * 180,Vector3D.Y_AXIS);
	matrix.transformVectors(verts3D,transform3D);
 
	Utils3D.projectVectors(worldMatrix,transform3D,verts2D,uvts);
 
	render.graphics.clear()
 
	render.graphics.beginBitmapFill(image1,null,false,false);
	render.graphics.drawTriangles(verts2D,indices,uvts,"negative");
 
	render.graphics.beginBitmapFill(image2,null,false,false);
	render.graphics.drawTriangles(verts2D,indices,uvts,"positive");
 
	e.updateAfterEvent();
 
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Z-Sorting

Comme je le disais un peu plus haut, on s'est un peu simplifié la vie en créant un objet3D convexe. Essayons maintenant de créer un objet concave, pour voir ce que ça donne.

On ne vas pas se prendre la tête à recréer un objet concave en particulier, on va se contenter des deformer notre “cylindre” pour le rendre concave. On peut le faire très facilement en réduisant progressivement le radius pour obtenir une spirale (je modifie aussi la position Y et la hauteur pour rendre le tout plus joli)

var i:int;
var rx:Number;
var posy:Number = 0
for(i=2;i<nbSegment;i++){
	a = (Math.PI*2) * (i/(nbSegment-1));
	rx = uvx * (i-1);
	verts3D.push(Math.cos(a)*radius,posy-halfRingHeight,Math.sin(a)*radius);
	verts3D.push(Math.cos(a)*radius,posy+halfRingHeight,Math.sin(a)*radius);
 
	posy += 1
	radius *=0.99;
	halfRingHeight += 1
 
	uvts.push(rx,0,0);
	uvts.push(rx,1,0);
 
	indices.push(int(index - 1),index,int(index - 2));
	indices.push(index-1,int(index+1),int(index));
 
	index +=2
}

Ce qui nous permet d'obtenir ça

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

si on regarde l'objet sous un certain angle, on peut remarquer des defaut d'affichage : certains triangles qui devraient apparaitre en arriere plan apparaisent au premier plan, et reciproquement. Ceci est du au fait plusieurs triangle situé en face de nous (donc avec le bon culling) peuvent se superposer.

Puisque le culling ne permet pas de regler le problème, comment faire ? Il va falloir trier les triangles “à la main” pour dire à flash d'afficher les triangles dans l'ordre, du plus loin au plus prés.

L'ordre d'affichage des triangles est définis par le tableau 'indices'. Par exemple, dans le tableau indices = [ 1,2,3 , 4,5,6 , 7,8,9 ]

le triangle défini par les points 1,2,3 sera dessiné en premier, suivi de celui défini par les points 4,5,6, puis 7,8,9.

Trier les triangles revient donc à modifier les valeurs contenues dans le tableau 'indices'

Il faut donc créér un objet par triangle, qui va contenir les indices relatif à chaque point du triangle et qui stockera également la distance de chaque triangle.

var indices:Vector.<int> = new Vector.<int>();
var triangles:Array = [];
 
indices.push(0,1,2);
indices.push(1,3,2);
 
triangles.push({ i0:0 , i1:1 , i2:2 , z:0 });
triangles.push({ i0:1 , i1:3 , i2:2 , z:0 });

Une fois la distance trouvé, on va trier nos objet par distance grace à Array.sortOn et mettre à jour les valeurs de notre tableau indices.

Vous vous demandez peut être comment on faire pour calculer la distance de chaque triangle, d'autant plus que notre objet subi des transformation matricielle :) C'est très simple, on va utiliser le paramètre “z” du tableau uvts après l'appel de Utils3d.projectVectors. La valeur “z” contenu dans le tableau est une valeur normalisée (comprise entre 0 et 1), mais peu importe puisque nous on veut juste les trier.

Pour récuperer le 'z' de chaque triangle, on va simplement additionner le 'z' de chaque uvts de chaque point du triangle (si on voulait obtenir la distance moyenne, il faudrait ensuite les diviser par 3 pour , mais puisqu'on veut simplement trier les triangles, pas besoin d'ajouter ce calcul superflu)

	Utils3D.projectVectors(worldMatrix,transform3D,verts2D,uvts);
 
	var i:int,len:int = triangles.length;
	var triangle:Object;
	for(i=0;i<len;i++){
		triangle = triangles[i];
		triangle.z = uvts[int(triangle.i0 * 3 + 2)] + uvts[int(triangle.i1 * 3 + 2)] + uvts[int(triangle.i2 * 3 + 2)]
	}
	triangles.sortOn("z",Array.NUMERIC);
 
	var id:int = 0;
	for(i=0;i<len;i++){
		triangle = triangles[i];
		indices[id++] = triangle.i0;
		indices[id++] = triangle.i1;
		indices[id++] = triangle.i2;
	}

Voila le code complet de la forme concave avec le z-sorting

import flash.display.BitmapData;
import flash.geom.Matrix3D;
import flash.geom.PerspectiveProjection;
import flash.geom.Vector3D;
 
var radius:Number = 250;
var nbSegment:int = 100;
var ringHeight:Number = 100;
var halfRingHeight:Number = ringHeight/2;
var uvx:Number = 1/(nbSegment);
 
var verts3D:Vector.<Number> = new Vector.<Number>();
 
verts3D.push(radius,-halfRingHeight,0);
verts3D.push(radius,halfRingHeight,0);
 
var a:Number = (Math.PI*2) / (nbSegment);
 
verts3D.push(Math.cos(a)*radius,-halfRingHeight,Math.sin(a)*radius);
verts3D.push(Math.cos(a)*radius,halfRingHeight,Math.sin(a)*radius);
 
var uvts:Vector.<Number> = new Vector.<Number>();
uvts.push(0,0,0);
uvts.push(0,1,0);
uvts.push(uvx,0,0);
uvts.push(uvx,1,0);
 
var verts2D:Vector.<Number>;
 
var indices:Vector.<int> = new Vector.<int>();
var triangles:Array = [];
 
indices.push(0,1,2);
indices.push(1,3,2);
 
triangles.push({ i0:0 , i1:1 , i2:2 , z:0 });
triangles.push({ i0:1 , i1:3 , i2:2 , z:0 });
 
var index:int = 4;
 
var i:int;
var rx:Number;
var posy:Number = 0
for(i=2;i<nbSegment;i++){
	a = (Math.PI*2) * (i/(nbSegment-1));
	rx = uvx * (i-1);
	verts3D.push(Math.cos(a)*radius,posy-halfRingHeight,Math.sin(a)*radius);
	verts3D.push(Math.cos(a)*radius,posy+halfRingHeight,Math.sin(a)*radius);
 
	posy += 1
	radius *=0.99;
	halfRingHeight += 1
 
	uvts.push(rx,0,0);
	uvts.push(rx,1,0);
 
	indices.push(int(index - 1),index,int(index - 2));
	indices.push(index-1,int(index+1),int(index));
 
	triangles.push({ i0:int(index - 1) , i1:index        , i2:int(index - 2) , z:0 });
	triangles.push({ i0:index-1        , i1:int(index+1) , i2:index          , z:0 });
 
	index +=2
 
}
 
verts3D.fixed = true;
uvts.fixed = true;
indices.fixed = true;
verts2D = new Vector.<Number>((verts3D.length / 3) * 2 ,true);
 
var transform3D:Vector.<Number> = verts3D.concat();
transform3D.fixed = true;
 
var render:Shape = new Shape();
render.x = stage.stageWidth/2;
render.y = stage.stageHeight/2;
addChild(render);
 
var image1:BitmapData = new Tof(0,0) as BitmapData;
var image2:BitmapData = new Tof2(0,0) as BitmapData;
 
var matrix:Matrix3D = new Matrix3D();
var contenerMatrix:Matrix3D = new Matrix3D();
stage.addEventListener(MouseEvent.MOUSE_MOVE,update);
 
 
var worldMatrix:Matrix3D = new Matrix3D();
var pers:PerspectiveProjection = new PerspectiveProjection();
var persMatrix:Matrix3D = pers.toMatrix3D();
var worldPosition:Vector3D = new Vector3D(0,0,pers.focalLength + radius);
 
function update(e:MouseEvent):void{
 
	matrix.identity();
	matrix.appendRotation(render.mouseY/render.y * 180,Vector3D.X_AXIS);
	matrix.appendRotation(render.mouseX/render.x * 180,Vector3D.Y_AXIS);
	matrix.transformVectors(verts3D,transform3D);
 
	worldMatrix.identity();
	worldMatrix.position = worldPosition;
	worldMatrix.append(persMatrix);
	Utils3D.projectVectors(worldMatrix,transform3D,verts2D,uvts);
 
	var i:int,len:int = triangles.length;
	var triangle:Object;
	for(i=0;i<len;i++){
		triangle = triangles[i];
		triangle.z = uvts[int(triangle.i0 * 3 + 2)] + uvts[int(triangle.i1 * 3 + 2)] + uvts[int(triangle.i2 * 3 + 2)]
	}
	triangles.sortOn("z",Array.NUMERIC);
 
	var id:int = 0;
	for(i=0;i<len;i++){
		triangle = triangles[i];
		indices[id++] = triangle.i0;
		indices[id++] = triangle.i1;
		indices[id++] = triangle.i2;
	}
 
	render.graphics.clear()
 
	render.graphics.beginBitmapFill(image1,null,false,false);
	render.graphics.drawTriangles(verts2D,indices,uvts,"negative");
 
	render.graphics.beginBitmapFill(image2,null,false,false);
	render.graphics.drawTriangles(verts2D,indices,uvts,"positive");
 
	e.updateAfterEvent();
 
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Il est important de préciser que c'est vraiment le tableau 'indices' qui définit ce qui est affiché ou pas. Si vous avez un tableau verts2D rempli avec 2000 valeurs (pour 1000 triangles) mais que votre tableau 'indices' en contient 3 (pour un seul triangle) , un seul triangle sera affiché.

Il est de fait possible de gérer une “display-list” en 3D avec des addChild/removeChild sans jamais toucher au tableau verts3D/verts2D/uvts

création d'un carrousel

Je crois qu'on a vu à peu prés tout (ce que je connais) concernant la manipulation d'un objet unique en 3D. Voyons maintenant comment créer un carroussel.

Puisqu'on travaille avec plusieurs objets 3D, je vais utiliser des classes pour écrire le code, je pense que ce sera plus simple et plus clair.

Pour créer notre carroussel, on a besoin de plusieurs Plane (rectangle3D) qui seront disposé autour d'un cylindre imaginaire et d'un contener dans lequel on va mettre nos plane et que l'on fera tourner. C'est à peu prés tout.

Vous vous demandez peut être comment on va appliquer la rotation du contener sur la position de chaque plane (elle même disposé et orienté dans un espace 3D) ? C'est très simple, bien plus que tout ce qu'on imagine : il suffit de multiplier les matrices ensemble (comme on a fait tout à l'heure pour gérer la perspective). Et pour ça, il suffit d'écrire

localMatrix3D.append(parentMatrix3D);

Que ce soit une Plane ou le contener, les 2 ont besoin de propriété x,y,z,rotationX,rotationY,rotationZ,scaleX,scaleY,scaleZ donc d'une Matrix3D. Le plus simple est donc de créer une classe commune qui étend Matrix3D :)

package  
{
	import flash.display.Graphics;
	import flash.geom.Matrix3D;
	import flash.geom.Vector3D;
	/**
	 * ...
	 * @author tlecoz
	 */
	public class Object3D extends Matrix3D
	{
 
		public var x:Number = 0;
		public var y:Number = 0;
		public var z:Number = 0;
		public var rotationX:Number = 0;
		public var rotationY:Number = 0;
		public var rotationZ:Number = 0;
		public var scaleX:Number = 1;
		public var scaleY:Number = 1;
		public var scaleZ:Number = 1;
 
		public function Object3D() 
		{
 
		}
 
		public function update(parentMatrix:Matrix3D=null):void {
 
			identity();
 
			appendScale(scaleX, scaleY, scaleZ);
			appendRotation(rotationX, Vector3D.X_AXIS);
			appendRotation(rotationY, Vector3D.Y_AXIS);
			appendRotation(rotationZ, Vector3D.Z_AXIS);
			appendTranslation(x, y, z);
 
			if (parentMatrix) append(parentMatrix);
 
		}
		public function draw(g:Graphics):void {
			//must be overrided
		}
	}
}

Ce qui nous permet d'écrire plus simplement la classe Plane

package  
{
	import flash.display.BitmapData;
	import flash.display.Graphics;
	import flash.geom.ColorTransform;
	import flash.geom.Matrix3D;
	import flash.geom.Vector3D;
	/**
	 * ...
	 * @author tlecoz
	 */
	public class Plane extends Object3D
	{
 
		public var vertices3D:Vector.<Number> = new Vector.<Number>();
		public var transform3D:Vector.<Number> = new Vector.<Number>();
		public var vertices2D:Vector.<Number> = new Vector.<Number>();
		public var uvts:Vector.<Number> = new Vector.<Number>();
		public var indices:Vector.<int> = new Vector.<int>();
 
		public var rectoAlpha:BitmapData;
		public var versoAlpha:BitmapData;
 
		public function Plane(rectoBd:BitmapData,versoBd:BitmapData) 
		{
			//cette partie n'est pas importante, mais je elle sera utile quand je gererais les evenement souris...
			rectoAlpha = rectoBd.clone();
			rectoAlpha.colorTransform(rectoAlpha.rect, new ColorTransform(1, 1, 1, 0.5));
			versoAlpha = versoBd.clone();
			versoAlpha.colorTransform(versoAlpha.rect, new ColorTransform(1, 1, 1, 0.5));
 
			var w2:Number = rectoBd.width / 2;
			var h2:Number = rectoBd.height / 2;
 
			vertices3D.push( -w2, -h2, 0);
			vertices3D.push( w2, -h2, 0);
			vertices3D.push( -w2, h2, 0);
			vertices3D.push( w2, h2, 0);
 
			transform3D = vertices3D.concat();
 
			uvts.push(0, 0, 0);
			uvts.push(1, 0, 0);
			uvts.push(0, 1, 0);
			uvts.push(1, 1, 0);
 
			indices.push(0, 1, 2);
			indices.push(1, 3, 2);
		}
 
		public override function update(parentMatrix:Matrix3D=null):void {
 
			super.update(parentMatrix);
			transformVectors(vertices3D, transform3D);
		}
 
		public function get realZ():Number {
			return uvts[2] + uvts[5] + uvts[8] + uvts[11];
		}
 
		public override function draw(g:Graphics):void {
 
			g.beginBitmapFill(rectoAlpha,null,false,false);
			g.drawTriangles(vertices2D,indices,uvts,"positive");
 
			g.beginBitmapFill(versoAlpha,null,false,false);
			g.drawTriangles(vertices2D, indices, uvts, "negative");
		}	
	}
}

Je pense que tout est clair, du moins j'espère :)

On peut enfin passer au code du carroussel, qui initialise et pilote les éléments.

Pour positionner nos planes, on pourrait reprendre la même méthode que pour le cylindre, calculer des sinus etc… Mais pfff, il faut aussi calculer l'orientation des planes pour qu'elles aient l'air d'etre disposé autour d'un cylindre et pas face à nous. Bref on va faire autrement ! :)

On va les disposer directement comme si elles étaient autours d'un cylindre, grace au matrix3D. Pour faire ça, il nous suffit de déplacer la plane sur l'axe des Z puis d'appliquer une rotation, le point d'origine deviendra notre pivot et la distance sur l'axe de Z sera le rayon de notre cercle. Ensuite, plutot que d'appliquer la transformation sur le tableau 'transform3D' (comme on fait d'habitude), on l'appliquera sur 'vertice3D' car on veut modifier la position de nos points de manière permanente.

for (i = 0; i < nbElement; i++) {
	plane = new Plane(recto, versoBd);
	plane.appendTranslation(0, 0, radius);
	plane.appendRotation(i / nbElement * 360, Vector3D.Y_AXIS);
	plane.transformVectors(plane.vertices3D, plane.vertices3D);
	planes.push(plane);
}

Voila le code complet du carroussel

package  
{
	import flash.display.BitmapData;
	import flash.display.Graphics;
	import flash.geom.ColorTransform;
	import flash.geom.Matrix3D;
	import flash.geom.PerspectiveProjection;
	import flash.geom.Utils3D;
	import flash.geom.Vector3D;
	/**
	 * ...
	 * @author tlecoz
	 */
	public class Carroussel extends Object3D
	{
		private var worldMatrix:Matrix3D = new Matrix3D();
		private var pers:PerspectiveProjection = new PerspectiveProjection();
		private var persMatrix:Matrix3D = pers.toMatrix3D();
		private var worldPosition:Vector3D;
 
		private var rectoBd:BitmapData = new Recto(0,0) as BitmapData;
		private var versoBd:BitmapData = rectoBd.clone();
 
		private var planes:Array = [];
 
 
		public function Carroussel(nbElement:int,radius:Number) 
		{
 
			versoBd.colorTransform(versoBd.rect, new ColorTransform(0, 0, 0, 1) );
 
			var i:int;
			var plane:Plane
			var recto:BitmapData;
 
 
			for (i = 0; i < nbElement; i++) {
 
				recto = rectoBd.clone();
				recto.colorTransform(recto.rect, new ColorTransform(1,1,1,1,-256+Math.random()*512, -256+Math.random()*512,-256+Math.random()*512,0));
 
				plane = new Plane(recto, versoBd);
				plane.appendTranslation(0, 0, radius);
				plane.appendRotation(i / nbElement * 360, Vector3D.Y_AXIS);
				plane.transformVectors(plane.vertices3D, plane.vertices3D);
				planes.push(plane);
			}
 
 
			worldMatrix.appendRotation(7, Vector3D.X_AXIS);
			worldMatrix.appendTranslation(0,0,pers.focalLength + radius);
			worldMatrix.append(persMatrix);
		}
 
		public override function update(parentMatrix:Matrix3D = null):void {
			super.update(parentMatrix);
 
			//contrairement à tout à l'heure ou on codait à l'échelle d'un objet complex
			//on ne trie pas les triangle mais les objets (pas besoin de trier les triangle à l'interieur d'une place car c'est un objet convex)
 
			var i:int, len:int = planes.length;
			var plane:Plane;
 
			for (i = 0; i < len; i++) {
				plane = planes[i];
				plane.update(this as Matrix3D);
			}
 
			planes.sortOn("realZ", Array.NUMERIC);
		}
 
 
		public override function draw(g:Graphics):void {
 
			update();
 
			var i:int, len:int = planes.length;
			var plane:Plane;
 
			for (i = 0; i < len; i++) {
				plane = planes[i];
				Utils3D.projectVectors(worldMatrix,plane.transform3D,plane.vertices2D,plane.uvts);	
				plane.draw(g);
			}
		}	
	}
}

Enfin, voici le code du Main , qui pilote le carroussel

package  
{
	import flash.display.Sprite;
	import flash.events.Event;
	/**
	 * ...
	 * @author tlecoz
	 */
	public class Main extends Sprite
	{
		private var render:Sprite = new Sprite();
		private var carroussel:Carroussel;
 
		public function Main() 
		{
			render.x = stage.stageWidth / 2
			render.y = stage.stageHeight / 2
			addChild(render);
 
			carroussel = new Carroussel(20, 900);
			carroussel.z = 500
 
			addEventListener(Event.ENTER_FRAME, update);
		}
		private function update(e:Event):void {
			render.graphics.clear();
 
			var rota:Number = (render.mouseX / render.x) * 180;
			carroussel.rotationY -= (carroussel.rotationY - rota) * 0.2
			carroussel.update();
			carroussel.draw(render.graphics);
		}	
	}
}

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Une fois qu'on a franchi le cap des vertices3D,vertices2D,uvts,indices,culling,zsorting,perspective et autres matrix3D , c'est plutôt simple vous trouvez pas ? :D

Bon cela dit, on a pas encore tout à fait fini car on a rarement besoin d'un carroussel qui ne réagit pas à la souris…Et puisqu'on dessine tout dans un seul objet graphics, on ne peut à priori pas savoir sur quel objet on a cliqué. Pourtant il le faut, et puisqu'il le faut on va le faire :)

gestion de la souris

Nos triangles sont dessiné, in fine, en 2D , et ils sont triés du plus loin au plus prés… Il suffit donc de boucler sur les triangles, du plus près au plus loin, et de vérifier le curseur de la souris ne rentre pas en collision avec l'un de nos triangle. C'est probablement super compliqué de convertir la position de la souris en 3D et de verifier la superposition avec un triangle en 3D ; mais puisqu'on dispose des coordonnées en 2D, autant coder toute cette partie en 2D. :)

Je ne sais pas faire ce genre de calcul (je ne rentrerais donc pas dans le détails) mais google est mon ami et grace à lui j'ai trouvé les formules qui vont bien, et qui m'ont permis d'écrire cette fonction (que j'ai placé au niveau de la classe Plane (mais qui devrait etre placé dans une classe DisplayObject3D idéalement, de laquelle Plane serait extends) )

override public function checkIfMouseCollide(_mouseX:Number, _mouseY:Number):void 
{
	super.checkIfMouseCollide(_mouseX, _mouseY);
 
	var mouseCollide:Boolean = false;
	var i:int, len:int = indices.length;
	var id0:int, id1:int, id2:int;
	var p1x:Number, p1y:Number, p2x:Number, p2y:Number, p3x:Number, p3y:Number, d1:Number, d2:Number, d3:Number;
 
	for (i = 0; i < len; i += 3) {
		id0 = indices[i];
		id1 = indices[int(i + 1)];
		id2 = indices[int(i + 2)];
 
		if (!mouseCollide) {
			p1x = vertices2D[int(id0*2)];
			p1y = vertices2D[int(id0*2 + 1)];
			p2x = vertices2D[int(id1*2)];
			p2y = vertices2D[int(id1*2 + 1)];
			p3x = vertices2D[int(id2*2)];
			p3y = vertices2D[int(id2*2 + 1)];
 
			d1 =  p1x * (p2y - mouseY) + p2x * (mouseY - p1y) + mouseX * (p1y - p2y);
			d2 =  p2x * (p3y - mouseY) + p3x * (mouseY - p2y) + mouseX * (p2y - p3y);
			d3 =  p3x * (p1y - mouseY) + p1x * (mouseY - p3y) + mouseX * (p3y - p1y);
 
			if ((d1 > 0 && d2 > 0 && d3 > 0) || (d1 < 0 && d2 < 0 && d3 < 0)) {
				mouseCollide = true;
				if (!_isOver) {
					_isOver = true;
				}
			}else {
				if (_isOver) {
					_isOver = false;
				}
			}
		}
	}
 
}

J'ai besoin de récuperer les position de la souris du stage vers mes planes. Mes planes sont situé dans le carroussel, le plus simple est de partager cette information au niveau de la classe Object3D

public var mouseCollisionDetection:Boolean = false;
protected var mouseX:Number;
protected var mouseY:Number;
 
public function checkIfMouseCollide(_mouseX:Number, _mouseY:Number):void {
	mouseCollisionDetection = true;
	mouseX = _mouseX;
	mouseY = _mouseY;
}

Au niveau du code du carroussel, on a maintenant besoin de lister nos triangles 2 fois au niveau de notre fonction update. Une première fois pour appliquer la transformation, récupérer les coordonnées 2D et tester s'il y a ou pas une activité de la souris. Et une deuxieme fois pour afficher les objets.

On ne peut pas tout faire en une fois car le test de collision doit être fait du plus prés au plus loin, alors que le rendu doit être appliquer du plus loin au plus proche.

var i:int, len:int = planes.length;
var plane:Plane;
var planeUnderMouse:Plane = null;
 
 
for (i = len - 1; i > -1; i--) {
	plane = planes[i];
 
	Utils3D.projectVectors(worldMatrix,plane.transform3D,plane.vertices2D,plane.uvts);
 
	plane.isOver = false;
	if(null == planeUnderMouse){
		plane.checkIfMouseCollide(mouseX, mouseY);
		if (plane.isOver) planeUnderMouse = plane;
	}
}
 
for (i = 0; i < len; i++) {
	plane = planes[i];
	plane.draw(g);
}

Vous trouverez le code final dans les sources, je ne vais pas l'ajouter car ça ajouterait 200 lignes similaire à celle écrite plus haut (et il y a suffisament de redondance de code comme ça ^^)

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Conclusion

OMG c'était long à écrire !

Les sources

Mais vous pouvez aussi proposer ces téléchargements tout au long du tutoriel, et pas seulement à la fin.

En savoir plus

Ici des liens intéressants en rapport avec cet article, des documentations, d'autres tutoriels, etc