Mélanger des objets : partie 1

Ce didacticiel suppose que vous connaissiez la définitions des différents noeuds, events et spécifications de base du VRML97.
Vous pourrez trouver toutes ces infos (en français) sur le site de 3Dnet.
N'oubliez pas que tous les ROUTE doivent se trouver tout à la fin du fichier.

But :

Nous avons une série de N objets.
Par défaut, ces objets utilisent N emplacements dans l'espace.
Nous voulons qu'en cliquant sur n'importe lequel de ces objets, leurs emplacements soient permutées aléatoirement.

En pratique :

Commençons par définir les positions que nous souhaitons. Ce sont des noeuds transform sans enfants. Dans cet exemple, nous n'en utiliserons que 6, mais vous pouvez le faire pour N.

DEF pos1 Transform {
}
DEF pos2 Transform {
translation 5 0 0 }
DEF pos3 Transform {
translation 5 5 0 }
DEF pos4 Transform {
translation 0 5 0 }
DEF pos5 Transform {
translation 10 0 0 } DEF pos6 Transform {
translation 10 5 0 }

Vous noterez qu'ici, nous utilisons un exemple simple. Vous pourriez rajouter des paramètres de rotation ou d'échelle pour chacun de ces noeuds.

Nous allons maintenant définir les objets, plus précisement les géomètries qui seront associées aux positions définies plus haut. On utilise des primitives, mais vous pouvez en fait utiliser n'importe quel type de noeud, Transform, Group, Material, etc...

DEF geom1 Shape {
      geometry Box {}
}
DEF geom2 Shape {
      geometry Sphere {}
}
DEF geom3 Shape {
      geometry Cone {}
}
DEF geom4 Shape { geometry Cylinder {} }
DEF geom5 Shape { geometry Text{string"A"} }
DEF geom6 Shape {
geometry IndexedLineSet {
coord Coordinate {
point [-.5 0 0 0 1 0 .5 0 0]
}
coordIndex [0 1 2 0 -1]
}
}
Nous avons donc une boite,une sphère, un cône, un cylindre, une lettre et un triangle, tous placés par défaut en 0 0 0 de notre scène. Nous allons les parenter un par un à chaque position afin qu'ils ne soient pas tous superposés au lancement de la scène.
DEF pos1 Transform {
	children DEF geom1 Shape {
		geometry Box {}
	}
}
DEF pos2 Transform {
	translation 5 0 0
	children DEF geom2 Shape {
		geometry Sphere {}
	}
}
DEF pos3 Transform {
	translation 5 5 0
	children DEF geom3 Shape {
		geometry Cone {}
	}
}
DEF pos4 Transform {
	translation 0 5 0
	children DEF geom4 Shape {
		geometry Cylinder {}
	}
}
DEF pos5 Transform {
	translation 10 0 0
	children DEF geom5 Shape {
		geometry Text{string"A"}
	}
}
DEF pos6 Transform {
	translation 10 5 0
	children DEF geom6 Shape {
		geometry IndexedLineSet {
			coord Coordinate { 
				point[-.5 0 0 0 1 0 .5 0 0]
			}
			coordIndex[0 1 2 0 -1]
		}
	}
}
Voir la scène

Toutes les géomètries sont maintenant réparties sur une aire rectangulaire.
Pour déclencher la permutation de position, nous utiliserons un ToucheSensor. Pour l'associer avec tous les objets, il suffit de le placer tel quel dans la scène.

DEF capteur TouchSensor{}

Maintenant, le script qui va faire la permutation de position. La fonction de permutation se déclenchera au moment ou l'on clique sur la sphere rouge. L'eventIn sera donc un SFTime.

TDEF permut Script {
	eventIn	SFTime touch
	url "javascript:
	function touch(){
	}
	"
}

Routons tout de suite notre événement touchTime vers le script.

ROUTE capteur.touchTime TO permut.touch
Voir la scène

Le capteur est actif, mais il lance une fonction qui pour l'instant ne fait rien.

Prenons le temps de comprendre le principe que nous allons utiliser. D'abord, que savons nous ?

  • Nous avons les noeuds Transform qui nous servent de position.
  • Nous avons les différentes géométries.
  • Par défaut, chaque géomètrie est parenté à un des noeuds Transform.
  • Nous voulons qu'en cliquant sur un des objets, les géomètries change aléatoirement de position parmi celles définies.

Les coordonnées des noeuds Transform peuvent être très complexes : translation, rotation, scale, centre et scaleOrientation.
Plutôt que de permuter toutes ses coordonnées, nous allons permuter les enfants de ces noeuds. Ce qui revient au même.

Notre script utilisera donc deux listes :

  • pos, qui contient tous les noeuds représentant les coordonnées (les Transform)
  • enfants, qui contient les géomètries (les Shape)

Nous utilisons pour celà des champs MFNode.

field MFNode pos []
field MFNode enfants []

Pour l'instant, ces liste sont vide. Ils nous faut les remplir avec les élément correspondant. Servons nous de l'instruction USE qui appelle les noeuds (coordonnées et géomètries) déjà définies.

field MFNode pos [USE pos1 USE pos2 USE pos3 USE pos4 USE pos5 USE pos6 ]
field MFNode enfants [USE geom1 USE geom2 USE geom3 USE geom4 USE geom5 USE geom6 ]

Il nous faut une fonction qui associe à chaque élément de la liste pos un élément choisis au hazard dans la liste enfants, mais sans jamais utilisé le même enfant.
Là, nous rentrons dans la programmation pur. Connaissance en javascript bienvenue !

Je prend le temps de vous expliquer 3 principes possibles.

Méthode de tirage au sort sans remise

Cela reviens à piocher au hazard et une par une toutes les cartes d'un jeu jusqu'à la dernière. Les cartes tirées ne sont pas remises dans le jeu. Ce qui veut dire qu'on ne peut jamais piocher deux fois la même carte.

Dans notre cas de figure, on veut un enfant choisis au hazard dans une liste enfants que l'on parente à une position issue d'une liste pos.

La première méthode :
1) On tire un enfant au hazard dans la liste enfants et on mémorise son nom dans un autre tableau mem. .(l'enfant fait toujours partie de notre liste enfants.)Puis, on l'associe au premier élément de la liste pos.
2) Pour l'enfant suivant, on vérifie si son nom est déjà dans notre tableau mem des enfants déjà utilisés :

- Si oui, on retire au hazard un autre enfant dans la liste enfants.
- Sinon, on mémorise son nom dans le tableau mem et on l'associe au 2ème éléments de la liste pos.

3) Ainsi de suite jusqu'à avoir tiré chacun des enfant de la liste enfants.

Le problème, c'est qu'on peut repiocher pas mal de fois, surtout vers la fin. :-(

Deuxième méthode :
1) On tire au hazard un enfant dans la liste enfants, on l'associe au 1er élément de la liste pos.
2) On enlève l'enfant choisis de la liste enfants et la pos correspondante de la liste pos.
3) On recommence jusqu'à épuisement des enfants, et donc des pos..

Ici, nous devons redimensionner et reclasser deux liste à chaque tirage.

Troisième méthode :
1) On mélange les éléments de la liste enfants
2) On tire le 1er enfant, on l'associe à la 1ère pos.
3) On tire le 2ème enfant, on l'associe à la 2ème pos.
4) Ainsi de suite jusqu'à épuisement des enfants et des pos.

Le truc, c'est que à chaque clic sur le capteur, on remélange la liste enfants. Puis on distribue le 1er enfant à la première pos, etc..

Nous utiliserons la 3ème méthode que je trouve au finale comme étant la plus simple et la plus efficace.
Qu'est ce que ça donne pour notre script ?
Il nous faut deux boucles :
- une pour mélanger les enfants
- une pour attribuer chaque enfant à chaque pos

Pour la boucle de mélange des enfants, il nous faut une donnée supplémentaire : un champ qui stockera temporairement le noeuds déplacé pendant le mélange.

field SFNode temp NULL

Voici la boucle pour le mélange des enfants. Le mélange se fait par permutation d'éléments. Ce principe permet de ne jamais avoir deux fois le même élément dans la liste après le mélange.

for (j = 0; j < pos.length; j++) {
	k = Math.floor(Math.random() * pos.length);
	temp = enfants [ j ] ;
	enfants [ j ] = enfants [ k ] ;
	enfants [ k ] = temp ;
}

Nous avons donc maintenant les éléments de la liste enfants mélangés de façon aléatoire. Plus de détails sur le fonctionnement de cette boucle ici.

La boucle pour associer les enfants aux pos est plus simple.

for (i=0;i<pos.length;i++){
pos [ i ] .children [ 0 ] = enfants [ i ] ;
}

La propriété children des noeuds Transform est un exposedField de type MFNode. Le script a directement accès aux children des noeuds pos qui sont définis dans le champ MFNode pos. Nous remplaçons seulement le premier des children, d'où le .[ 0 ] à la suite de children.

Voici le code final du script

DEF permut Script {
 eventIn	SFTime touch
 field MFNode pos [USE pos1 USE pos2 USE pos3 USE pos4 USE pos5 USE pos6 ]
 field MFNode enfants [USE geom1 USE geom2 USE geom3 USE geom4 USE geom5 USE geom6 ]
 field SFNode temp NULL
 url "javascript:
 function touch(){
	for (j = 0; j < pos.length; j++) {
		k = Math.floor(Math.random() * pos.length);
		temp = enfants [ j ] ;
		enfants [ j ] = enfants [ k ] ;
		enfants [ k ] = temp ;
	}
	for (i=0;i<pos.length;i++){
		pos [ i ] .children [ 0 ] = enfants [ i ] ;
	}
}
"
}

Voir la scène Voir le source

Pour aller plus loin :

Pour l'instant, à chaque lancement de notre scène, les objets ont toujours la même position. Ce serait bien mieux, si au lancement les objets étaient déjà mélangés.

Pour exécuter le mélange à l'ouverture de la scène, on utilise la fonction standart initialize.

function initialize(){
}

Cette fonction ne fait en fait qu'exécuter la fonction touch

function initialize(){
	touch();
}

A chaque chargement de la scène, les objets auront une positions différentes. Essayez et vous verrez !

Voir la scène Voir le source

Surprise :

Vous avez tout compris du premier coup?
Félicitations, vous êtes très fort !

Petite question subsidiaire pour départager les ex-aequos :
Combien y a t'il de dispositions différentes possibles pour les 6 objets de cette scène ?

36 256 720 1 296
Solution
Valid XHTML 1.0 Strict Valid CSS!