ADD et REMOVE Children

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 :

Le but est d'arriver à parenter interactivement une sphère à plusieurs objets différents.
Au lancement de la scène, notre sphère n'a pas de parent.
Lorsque nous cliquerons sur une boite, nous parenterons la sphère soit à une boite animée en translation, soit à un cone déplaçable, ou bien à aucun objet.
Nous utiliserons pour cela les méthodes AddChildren et removeChildren.

Rappel de base:

Tout d'abord, les méthodes addChildren et removeChildren ne s'appliques qu'aux noeuds possèdant un exposedField children. Il existe 5 noeuds VRML standarts ayant ces caractérisiques :

  • Anchor
  • Bilboard
  • Collision
  • Group
  • Transform

L'exposedField children est de type MFNode, ainsi que les méthodes addChildren et removeChildren.

Notes : ils existent des PROTOS qui possèdent aussi ces méthodes.

Les objets:

Afin de bien illustrer les changements de parent, nous allons utiliser 4 objets :

  • un cube rouge animé en translation qui sera le parent cible.
  • un cone jaune déplaçable qui sera le parent source.
  • une sphère bleu qui sera la sphere qui changera de parent.
  • un boite qui servira à faire changer de parent à notre sphere.

Les noms des objets correspondent aux noms utilisés lors de l'utilisation de la commande DEF. Ceux-ci sont indiqués en caractères gras.


Nous allons d'abord créer le parent cible et son interpolateur :

DEF	cible Transform {
	translation	-2.5 5 0
	children [
		Shape {
			geometry Box {			}
			appearance Appearance {
				material Material {
					diffuseColor .5 0 0
				}
			}
		}
	] 
}
DEF	timer TimeSensor {
	cycleInterval 3
	loop TRUE
}
DEF	interpoleur PositionInterpolator {
	key [0 .5 1]
	keyValue [-2.5 5 0 2.5 5 0 -2.5 5 0]
}
ROUTE timer.fraction_changed TO	interpoleur.set_fraction
ROUTE interpoleur.value_changed	TO cible.translation

Voir la scène Voir le source

Nous passons maintenant au parent source, c'est à dire le cone jaune déplaçable :

DEF	source Transform {
	translation	-2 0 0	
	children [
		Shape {
			geometry Cone {		}
			appearance Appearance {
				material Material {
					diffuseColor .5 .5 0
				}
			}
		}
		DEF	mover PlaneSensor {
		}
	]
}
ROUTE mover.translation_changed	TO source.translation

Voir la scène Voir le source

Ici , la sphère bleu qui sera la sphere qui changera de parent :

DEF sphere Transform {
	translation	2 0 0
	children [ Shape {
			geometry Sphere	{			}
			appearance Appearance {
				material Material {
					diffuseColor 0 0 .5
				}
			}
		}
	]
}

Pour l'instant, ce noeud sphere n'a pas de parent. Il est donc placé dans le sytème de coordonnées de la scènes. Il est placé au point de coordonnées X= 2, Y = 0 et Z = 0.

Voir la scène Voir le source

Reste notre boite qui sert de "parenteur". Nous lui associons un TouchSensor nommé change en les plaçant tous deux comme enfant d'un noeud Transform. Nous plaçon légèrement plus bas afin qu'il ne soit pas masqué par un autre objet :

DEF	boite Transform	{
	translation	0 -2 0
	children [
		Shape {
			geometry Box {
				size .5 .5 .5
			}
			appearance Appearance {
				material Material {
					diffuseColor 1 1 1 
				}
			}
		}
		DEF	change TouchSensor {
		}
	]
}

Voir la scène Voir le source

Le script:

Pour gérer ce changement de parent, nous avons besoin d'un noeud script que nous appellerons parenteur.

DEF	parenteur Script {
}

Ce script sera appelé au moment on l'on clique sur la boite. Nous ferons la liaison de l'eventOut touchTime du TouchSensor change vers le script parenteur via un ROUTE. Le script doit donc avoir un eventIn de type SFTime pour recevoir l'évènement touchTime. Nous appellerons cet eventIn touch. Cet eventIn appellera la fonction du même nom, la fonction touch() :

DEF	parenteur Script {
	eventIn	SFTime touch
	url "javascript:
	function touch(){
	}
	"
}
ROUTE change.touchTime TO parenteur.touch

Le script doit d'abord connaitre quel objet nous parenterons. Nous ajoutons donc un champ enfant_temp de type SFNode qui fera référence au noeud sphere de la scène via la commande USE.

field SFNode sphere USE	sphere

Le script à aussi besoin de savoir à quel objet nous parenterons le noeud sphere. Nous allons dans un premier temps nous contenter de la parenter au cube rouge animé qui est le noeud cible. Nous ajoutons donc un autre champ de type SFNode qui fera référence au noeud cible :

field SFNode cible USE	cible

Maintenant, la fonction touch(). Cette fonction utilise la méthode addChildren. Mais cette méthode est du type MFNode, et notre noeud cible est lui de type SFNode. Ces deux types ne sont pas compatibles. Nous allons donc changer le type du champ enfant_temp. Nous le changeons de SFNode en MFNode. Dans le premier élément de ce MFNode, nous mettrons la référence à notre noeud sphere. Cette manipulation donnera au final exactement le même résultat :

field MFNode enfant_temp [USE sphere]
url"javascript:
function touch(){
	cible.addChildren = enfant_temp ;
}

Voir la scène Voir le source

En testant la scène, on voit tout de suite un problème flagrant : le script à dupliquer notre sphere !!
Un se retrouve parenter au noeud cible et c'est ce que nous voulions. Mais le premier sphere est toujours là...
De plus, et bien que celà ne se voit pas, à chaque nouveau clic sur la boite, nous dupliquons un autre sphere. Si nous ne le voyons pas, c'est parce que tous ces noeuds sphere sont superposées les uns sur les autres.

Occupons nous déja du premier problème : faire diparaitre le premier noeud sphere.
Pour celà, nous allons mettre notre noeud Tranform sphere dans un group que nous nommerons root.

DEF root Group {
	children [
		DEF sphere Transform {
			translation	2 0 0
			children [ Shape {
					geometry Sphere	{}
					appearance Appearance {
						material Material {
							diffuseColor 0 0 .5
						}
					}
				}
			]
		}
	]
}

Nous modifions notre fonction touch() de façon que en même temps que nous ajoutons comme enfant le noeud sphere au noeud cible, nous enlevons l'enfant sphere du noeud root.
Nous ajoutons aussi un champ de type SFNode qui fait référence au noeud root :

DEF	parenteur Script {
	eventIn	SFTime touch
	field SFNode cible USE cible
	field SFNode root USE root
	field MFNode enfant_temp [USE	sphere]
	url"javascript:
	function touch(){
		cible.addChildren = enfant_temp ;
		root.removeChildren = enfant_temp ;
	}
	"
}

Voir la scène Voir le source

C'est déja mieux.
De plus, nous avons régler le 2ème problème en même temps. Le script ne peut pas ajouter un autre noeud sphere comme enfant au noeud cible, vu que sphere est déja enfant de cible.


Maintenant, occupons-nous de parenter sphere à l'objet suivant, le cone jaune qui est en fait le noeud source.
Vu que notre noeud sphere peut être parenté à au moins trois objets, il nous faudrait une liste des parents possibles. Cette liste est facile à faire : nous utilisons un champ MFNode parents qui référence chacun des parents possibles. Et du même coup, nous n'avons plus besoin des différents champ SFNode qui recensent actuellement les parents possibles. Notre script devient donc :

DEF	parenteur Script {
	eventIn	SFTime touch
	field MFNode parents [USE root USE cible USE source ]
	field MFNode enfant_temp [USE	sphere]
	url"javascript:
	function touch(){
		......
	}
	"
}

Nous rajoutons un champ indice de type SFInt32 qui enregistrera l'indice du parent courant dans la liste parents.

field SFInt32 indice 0

Nous modifions notre fonction touch() de façon à incrémenter indice de 1 à chaque clic. Nous mettons une condition qui vérifiera que notre indice n'excéde pas le nombre de parent possible contenus dans la liste parents. Si c'est le cas, nous remettons indice à 0. Attention, il y a un petit piège : indice ne dois pas être supérieur au nombre d'éléments de parents - 1 !!!

function touch(){
		if (indice < parents.length-1) {
			indice ++ ;
		}
		else {
			indice = 0 ;
		}
		......
	}

Ici, il faut faire attention et être logique :
Si nous incrémentons indice tout de suite, indice correspondra à l'indice du futur parent. Ce qui nous arrange pour parenter sphere au futur parent. Mais ceci va singulièrement nous compliquer la vie pour enlever sphere de son précédent parent. Car l'indice du précédent parent est en réalité indice-1. Mais comme nous avons un sytème de remise à 0 de indice, nous allons avoir un problème dans le cas ou le précédent parent sera le derniers de la liste parents. Car dans ce cas, nous ne pourrons pas utiliser indice-1.

Pour contourner le problème, il y a plus simple : nous enlevons sphere du parents d'indice courant, nous incrémentons indice, puis nous parentons sphere au parents d'indice courant. sphere n'a donc pas de parent le temps que dure l'incrémentation de indice.
Vous remarquerez que avec cette technique, nous n'avons pas à nous tracasser avec des histoire de indice-1 et indice si c'est le dernier de parents.

Voici donc la fonction touch() telle qu'elle doit être :

function touch(){
	parents[indice].removeChildren = enfant_temp ;
	if (indice < parents.length-1) {
		indice ++ ;
	}
	else {
		indice = 0 ;
	}
	parents[indice].addChildren = enfant_temp ;
}

Voir la scène Voir le source

Pour aller plus loin :

Il est maintenant facile de rajouter un parent potentiel à notre sphère. Il suffit de créer un noeud qui possédent un exposedFields children, de le nommer avec DEF et de d'ajouter sa référence dans le champs parents du script.

Voir la scène Voir le source


Nous pouvons aussi envisager de rajouter un bouton qui permettrait de faire "marche arrière" dans l'ordre de parentage. Il convient pour cela :
- de rajouter un objet avec un TouchSensor :

DEF	boite2 Transform	{
	translation	2 -2 0
	children [
		Shape {
			geometry Box {
				size .5 .5 .5
			}
			appearance Appearance {
				material Material {
					diffuseColor 1 1 1 
				}
			}
		}
		DEF	change2 TouchSensor {
		}
	]
}

- rajouter un eventIn de type SFTime au script ainsi que les ROUTE :

eventIn	SFTime touch2
.....
ROUTE change2.touchTime TO parenteur.touch2

- le script demande lui plus de modifications. Il faut en effet rajouter un fonction touch2() qui décrémentera indice, avec là aussi une condition qui vérifie que indice ne tombe pas en dessous de 0 et le remette à la valeur nombre d'éléments de parents-1 le cas échéant :

function touch2(){
	if (indice > 0) {
		indice -- ;
	}
	else {
		indice = parents.length-1 ;
	}
}

Les function touch() et touch2() feront toutes les deux appels à deux autre fonctions :
- une fonction add() pour ajouter un enfant
- une fonction remove() pour enlever un enfant.

function touch(){
	remove() ;
	if (indice < parents.length-1) {
		indice ++ ;
	}
	else {
		indice = 0 ;
	}
	add() ;
}
function touch2(){
	remove() ;
	if (indice > 0) {
		indice -- ;
	}
	else {
		indice = parents.length-1 ;
	}
	add() ;
}
function add(){
	parents[indice].addChildren = enfant_temp ;
}
function remove(){
	parents[indice].removeChildren = enfant_temp ;
}

Voir la scène Voir le source


Dernière petite variante possible, nous ajoutons un troisième bouton qui lui parentera sphere à un objet au hasard.
Il faut donc :
- rajouter un bouton
- un eventIn, une ROUTE et une fonction.

La fonction touch3() est assez simple et posséde une structure très similaire au fonction touch() et touch2(). Sa particularité est d'utiliser une fonction standard Math.random() multipliée par le nombre d'éléments de parents -1, que nous arrondissons pour avoir un indice entier naturel :

function touch3(){
	remove() ;
	indice = Math.round((parents.length-1)*Math.random()) ;
	add() ;
}

Voir la scène Voir le source

Valid XHTML 1.0 Strict Valid CSS!