Billes de billard

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 allons créez un systèmes qui permet de mélanger les billes sur un plateau de billard.
Les billes peuvent être placées n'importe où sur le plateau qui mesure 210 cm x 105 cm à 82.5 cm de haut. Les billes ne doivent pas s'interpénétrer.
Ils y a 16 billes : 7 rouges, 7 jaunes et une noire de 5.08 cm de diamètre et 1 blanche de 4.7 cm de diamètre
Au chargement de la scène, les billes seront placées aléatoirement sur le plateau.

En pratique:

Commençons par définir la première bille rouge. C'est une sphère rouge de 2.54 de rayon. Nous la nommons B1 :

DEF	B1 Transform {
	children DEF billerouge Shape {
		geometry Sphere	{
			radius 2.54
		}
		appearance Appearance {
			material Material {
				diffuseColor .8 0 0
				specularColor .5 .5 .5
				emissiveColor .15 0 0
				ambientIntensity 0
			}
		}
	}
}

Vous noterez qu'ici, nous ne précisons aucune information quant à la position de la bille. Elle est donc située par défaut au centre de la scène au point x=0 , y=0 et z=0.
Nous avons définis comme enfant de B1 la Shape billerouge que nous réutilisons pour les autres billes rouges avec la commande USE.

DEF	B2 Transform {
	children USE billerouge
}
DEF	B3 Transform {
	children USE billerouge
}
DEF	B4 Transform {
	children USE billerouge
}
DEF	B5 Transform {
	children USE billerouge
}
DEF	B6 Transform {
	children USE billerouge
}
DEF	B7 Transform {
	children USE billerouge
}
Les billes rouges B1, B2, B3, B4, B5, B6 et B7 sont toutes définies. Pour les jaunes, c'est le même principe avec une Shape billejaune. Ces billes jaunes seront nommées à la suite des billes rouges, soit B8, B9, B10, B11, B12, B13 et B14
DEF	B8 Transform {
	children DEF billejaune Shape {
		geometry Sphere	{
			radius 2.54
		}
		appearance Appearance {
			material Material {
				diffuseColor .8 .63 0
				specularColor .5 .5 .5
				emissiveColor .14 .11 0
				ambientIntensity 0
			}
		}
	}
}
DEF	B9 Transform {
	children USE billejaune
}
DEF	B10 Transform {
	children USE billejaune
}
DEF	B11 Transform {
	children USE billejaune
}
DEF	B12 Transform {
	children USE billejaune
}
DEF	B13 Transform {
	children USE billejaune
}
DEF	B14 Transform {
	children USE billejaune
}
La 15ème bille, la noire :
DEF	B15 Transform {
	children DEF billenoire Shape {
		geometry Sphere	{
			radius 2.54
		}
		appearance Appearance {
			material Material {
				diffuseColor 0 0 0
				specularColor .502 .502 .502
				emissiveColor .149 .149 .149
				ambientIntensity 0
				shininess .369
			}
		}
	}
}
Reste la 16ème, la bille blanche. Attention, son rayon est plus petit !
DEF	B16 Transform {
	children DEF billeblanche Shape {
		geometry Sphere	{
			radius 2.35
		}
		appearance Appearance {
			material Material {
				diffuseColor .75 .75 .75
				specularColor .5 .5 .5
				emissiveColor .15 .15 .15
				ambientIntensity 0
			}
		}
	}
}

Toutes ces billes sont situées au même emplacement, le centre de la scène de coordonnées [ 0 0 0 ].

Créons une simple boite verte en guise de plateau : nous lui donnons les dimensions suivantes.
L = 210, l = 105 et H = 82.5

	Shape {
    	appearance	Appearance {
    		material	Material {
				diffuseColor .21 .44 0
				emissiveColor .09 .18 0
				ambientIntensity 0
				shininess 0
    		}
    	}
    	geometry	Box { size 210 82.5 105 }
	}

Ce plateau est situé lui aussi au point de coordonnées [0 0 0 ] de la scène.


Passons maintenant au script. Appelons le placeur. Il contient une fonction standart initialize(). Cette fonction nous oblige à mettre la propriété mustEvaluate du script à TRUE

DEF	placeur Script {
	mustEvaluate TRUE
	url	"javascript: 
	function initialize(){
	}
	"
}

Le script fera appel à la position de chacune des billes via un champ bille de type MFNode. Ce champ référence les billes de la scènes avec USE.

field MFNode bille [ USE B1 USE B2 USE B3 USE B4 USE B5 USE B6 USE B7 USE B8 
		USE B9 USE B10 USE B11 USE B12 USE B13 USE B14 USE B15 USE B16 ] 

Nous utilisons un champ postemp de type SFVec3f qui contiendra une position générée aléatoirement. Toutefois, nous savons que les billes seront toutes placées à la même hauteur. (coordonnée Y).Cette hauteur est : la moitié de la hauteur du tapis + le rayon des billes, soit 82.5/2 + 2.54 = 43.79. Nous définissons donc à l'avance cette coordonnée dans la valeur par défaut du champ postemp
(Nous verrons plus tard comment nous occuper du cas de la bille blanche)

field SFVec3f postemp 0 43.79 0

Nous définissons maintenant une boucle qui va affecter des valeurs aléatoires aux coordonnées X et Z de postemp. La valeur X variant de -105 à 105 et la Z de -52.5 à 52.5. (Le tapis étant centré sur [ 0 0 0], et sa longueur étant de 210, il va en réalité de la coordonnée X = - ( L/2) à X = L/2... Idem pour la largeur.)

for (i=0;i<16;i++){
			postemp.x = Math.random()*210 - 105 ;
			postemp.z = Math.random()*105 - 52.5 ;
			bille[i].translation = postemp ;
		}

Voir la scène Voir le source

Le tapis du billard est entouré d'un rebord qui empêche les billes de tomber par terre. Par conséquent, c'est l'extérieur de la bille qui doit atteindre la coordonnées X = 105 ou X = -105 (idem pour la largeur). L'amplitude est donc la longueur du tapis moins le diamétre de la bille. Comme nous avons vus précédemment qui fallait recentrer ces coordonnées, il faut, dans notre opération, soustraire la moitié de la longueur moins le rayon de la bille. Ce qui donne :

for (i=0;i < 16;i++){
			postemp.x = Math.random()*204.92 - 102.46 ;
			postemp.z = Math.random()*99.92 - 49.96 ;
			bille[i].translation = postemp ;
		}

Voir la scène Voir le source


Maintenant; il nous faut régler un problème : avec ce script, les billes sont placées aléatoirement. Mais il se peut que les billes se chevauchent. Les positions générées aléatoirement ne tiennent pas compte des emplacements occupés par les billes déjà placées.
Ce qu'il nous faut, c'est une fonction qui pour chaque bille génère une position aléatoire et vérifie que cette position n'est pas trop voisine d'une position déjà affectée.
Si c'est le cas, la fonction se relance.
Sinon, elle place la bille suivante.

La fonction pour générer une position aléatoire existe déjà : ce sont les instructions contenues dans la boucle de la fonction initialize(). Plaçons ces instructions dans une fonction genpos() qui sera exécuter par la function initialize()

function genpos(i){
		postemp.x = Math.random()*204.92 - 102.46 ;
		postemp.z = Math.random()*99.92 - 49.96 ;
		bille[i].translation = postemp ;
	} 
	function initialize(){
		for (i=0;i<16;i++){
			genpos(i);			
		}
	}

Remarquez que nous passons à la fonction genpos() l'argument i. Cet argument est important car il est nécessaire pour savoir à quelle bille du champ MFNode bille nous affectons la postemp générée.

Rajoutons à notre fonction fonction genpos(i) des instructions qui vérifieront si postemp correspond à une position déjà occuper par une une bille.
Pour celà, nous utilisons une boucle. Cette boucle vérifie si le centre de la bille que nous plaçons (autrement dit, postemp) se trouve à une distance inférieur à son diamètre du centre d'une des billes déjà placée.

function genpos(i){
 postemp.x = Math.random()*204.92 - 102.46 ;
 postemp.z = Math.random()*99.92 - 49.96 ;
 bille[i].translation = postemp ;
 for (j=0;j<i;j++){
  var dist = Math.sqrt( (bille[j].translation.x - postemp.x)*
(bille[j].translation.x - postemp.x)
+ (bille[j].translation.z - postemp.z)*
(bille[j].translation.z - postemp.z) ) ; } }

Nous connaissons maintenant la distance entre la bille que nous plaçons et la jème placée. Nous vérifions si cette distance est supérieur à deux rayons de bille, soit un diamètre : D = 5.08 cm

if ( dist < 5.08 ){
}

Il y a deux cas :

  • la distance est supérieur
  • la distance est inférieur.

Si la distance est supérieur, nous passons à la boucle suivante de la fonction initialize().


Sinon, nous pouvons déjà arrêter de vérifier les distance. Nous sortons de la boucle for de la fonction genpos(i) avec l'instruction break.

if ( dist < 5.08 ){
	break ;
}

Nous voilà de retour dans la boucle for de la fonction initialize(). Nous savons que postemp ne convenait pas, mais le script lui ne le sait pas.Nous enregistrons donc cet état dans un field nommé collide de type SFBool. Par défaut, il sera à FALSE.

field SFBool collide FALSE

C'est dans la boucle for de la fonction genpos(i) qu'il faut enregistrer cet état.

if ( dist < 5.08 ){
	collide = true ;
	break ;
}
else { 
	collide = false ;
}

Mais c'est dans la boucle for de la fonction initialize() que nous utiliserons cet donnée. Car si collide = TRUE, il faut relancer la fonction genpos(i). Et il faudra la relancer tant que collide sera égal à TRUE. Nous utilisons pour celà l'instruction while.

function initialize(){
	for (i=1;i<16;i++){
		genpos(i) ;
		while (collide){
			genpos(i) ;
		}
	}
}

Voir la scène Voir le source

Reste le cas de la bille blanche. C'est la dernière du champ MFNode bille. Nous ajoutons donc une condition dans la boucle de la fonction initialiaze(). Si c'est la dernière bille, on change la coordonnées Y de postemp à 43.6 (Hauteur du tapis/ 2 + rayon de la bille blanche) De cette façon, la bille blanche sera correctement posé sur le tapis.

if (i > 14) postemp.y = 43.6 ;

Les plus perspicaces remarqueront qu'ils faudrait aussi modifier les valeurs associées au Math.random() de postemp.x et postemp.z de la fonction genpos(i).

Sinon, nous pouvons optimiser ce script en plaçant l'affectation de postemp à bille[i].translation après avoir généré une postemp vérifiée. C'est à dire après le while de la fonction initialize().

Voir la scène Voir le source

Pour aller plus loin :

Nous pouvons rajouter un bouton qui permet de remélanger les billes. Il suffit de rajouter dans un noeud Transform une boite et un touchSensor. Plaçons ce bouton en avant de la boite qui sert de tapis. Le capteur sera nommé melange.

Transform {
	translation	0 0 60
	children [
		Shape {
			geometry Box {size 15 5 5}
		}
		DEF melange TouchSensor{}
	]
}

Nous ajoutons un eventIn touch de type SFTime au script placeur.

eventIn SFTime touch

Cet eventIn lancera une fonction touch() qui lancera la fonction initialize().

function touch(){
	initialize() ;
}

Nous routons le touchTime de melange vers l'eventIn touch du script placeur.

ROUTE sensor.touchTime TO placeur.touch

Voir la scène Voir le source

Et pour remercier les courageux qui sont allez jusqu'au bout de l'exercice, voici le code d'un plateau de billard tout fait !

Voir l'objet Voir le source

Valid XHTML 1.0 Strict Valid CSS!