Les interfaces 2 : les contrôleurs

Les interfaces 2 : les contrôleurs

A ce stade nous savons créer une fonction et afficher une fenêtre fournie de contrôleurs d'attributs, pouvant manipuler les attributs d'un objet :

# crée un slider automatiquement lié à l'attribut donné
cmds.attrFieldSliderGrp(attribute = 'monObjet.scaleX')
# crée un slider et une color box automatiquement liés à l'attribut donné
cmds.attrColorSliderGrp(attribute = 'maLumiere.color')
# crée le meilleur controleur pour l'attribut donné et le lie automatiquement
cmds.attrControlGrp(attribute = 'monObjet.translateY')
# ...

Nous allons voir maintenant comment contrôler nos propres fonctions.

Reprenons notre fonction ligneColonnes() :

def ligneColonnes(nombre, hauteur, espacement) :
    '''Cree x colonnes cotes à cotes, de hauteur et espacement donnes'''
    for i in(nombre) : # fais ceci "nombre" fois
        cmds.polyCube(h = hauteur) # cree un cube de hauteur donnee
        cmds.move(i*espacement,0,0) # le deplace selon l'espacement donne

Il serait utile d'avoir un slider gérant le nombre de colonnes, un autre leur hauteur et un dernier leur espacement. Vous comprenez qu'on ne peut le faire au moyen de contrôleurs d'attributs : le nombre de colonnes est variable, leur position dépend de plusieurs paramètres, etc…

Contrôle statique

Abandonnons donc ces contrôleurs d'attributs et penchons-nous sur les contrôleurs standard.

Ils contiennent tous un argument “command” (ou “changeCommand” et “dragCommand” pour les sliders), à l'image de ce que nous avons déjà utilisé pour faire des boutons créant des objets :

# ce bouton lance la ligne de commande "cmds.polySphere()" quand on appuie dessus
cmds.button(label = 'Créer une sphère', command = 'cmds.polySphere()') 

Hors la commande que nous lui demandons, “polysphere()”, est… une fonction. Et nous savons faire des fonctions. Plus besoin de se cantonner à ce qui existe déjà, nous pouvons ordonner à notre contrôle de faire ce que nous voulons. Remplaçons la fonction polySphere par notre fonction ligneColonnes :

# ce bouton lance la fonction "ligneColonnes()" quand on appuie dessus
cmds.button(
     label = 'Créer une ligne de colonnes',
     command = 'cmds.ligneColonnes(nombre = 5, hauteur = 10, espacement = 2)')
# on a indiqué en plus les arguments que demande la fonction

Ainsi quand on appuie sur ce bouton nous appelons notre fonction, et il se crée une ligne de 5 colonnes de hauteur 10 et espacées de 2.

Une action qui ne varie pas, qui fait une chose bien précise, par exemple “toutSupprimer”, “creer10Lumieres”, “polyCube”, etc… peut être contrôlée simplement de cette manière, en mettant la fonction dans l'argument “command” d'un contrôleur.

Cela fonctionne très bien, mais dans notre cas nous voulons un slider. Essayons :

cmds.intSliderGrp(
     label = 'nombre',
     changeCommand = 'cmds.ligneColonnes(nombre = 5, hauteur = 10, espacement = 2)')

Le slider appelle bien notre fonction quand on change sa valeur, mais fait toujours la même chose : en effet les arguments sont fixes (5, 10 et 2), c'est un contrôle statique.

Dans notre cas il faudrait que lorsqu'on modifie la valeur du slider on fasse varier un des paramètres. Devenons dynamiques.

Contrôle dynamique

Une fonction lambda est une micro-fonction tenant en une ligne, sans nom, qui sert a faire une petite action ponctuelle. Dans notre cas cette petite merveille peut récupérer en direct la valeur du contrôleur et modifier ainsi dynamiquement une valeur que l'on veut envoyer à ligneColonnes().

On utilise cette fonction de cette manière :

lambda variable : retour

La fonction renvoie ce que l'on demande dans “retour”.

Dans notre cas, on aimerait qu'elle renvoie notre commande…

'cmds.ligneColonnes(nombre = 5, hauteur = 10, espacement = 2)'

… mais en faisant varier la valeur 5. Pour que cela varie il faut une variable. Concaténons (lions, mettons bout à bout) notre commande (qui est une chaine de caractères) et une variable :

'cmds.ligneColonnes(nombre = ' + x + ', hauteur = 10, espacement = 2)'

Nous avons indiqué la fin de la chaine de caractère par un guillemet, avons placé notre variable, puis avons indiqué que la chaine de caractère redémarrait avec un autre guillemet. Ainsi, si “x” vaut 10, la chaine de caractère deviendra :

'cmds.ligneColonnes(nombre = 10, hauteur = 10, espacement = 2)'

Il reste une dernière étape : la fonction lambda préfère qu'on lui indique quel est le type de valeur de la variable (entier, nombre à virgule, etc…), pour savoir quoi en faire. pour lui indiquer, on écrit (les plus courants) :

int(x) # pour un entier
float(x) # pour un nombre à virgule
str(x) # pour une chaine de caractères (string)

Ici nous aurons un entier, il ne peut y avoir 2,57 colonnes. On a donc :

'cmds.ligneColonnes(nombre = ' + int(x) + ', hauteur = 10, espacement = 2)'

La fonction lambda entière est ainsi :

lambda x : 'cmds.ligneColonnes(nombre = ' + int(x) + ', hauteur = 10, espacement = 2)'

Et maintenant, qu'en fait-on? Et bien on la place simplement dans “changeCommand” de notre contrôleur :

cmds.intSliderGrp(
     label = 'nombre',
     changeCommand = lambda x : 'cmds.ligneColonnes(nombre = ' + int(x) + ', hauteur = 10, espacement = 2)')

Et voila, notre slider contrôle dynamiquement le nombre de colonnes !

Et là on se dit: cool, c'est réglé, on n'a plus qu'à faire les autres ! Oui mais… non. Essayons :

cmds.intSliderGrp(
     label = 'nombre',
     changeCommand = lambda x : 'cmds.ligneColonnes(nombre = ' + int(x) + ', hauteur = 10, espacement = 2)')
cmds.floatSliderGrp(
     label = 'hauteur',
     changeCommand = lambda x : 'cmds.ligneColonnes(nombre = 5, hauteur = ' + float(x) + ', espacement = 2)')
cmds.floatSliderGrp(
     label = 'espacement',
     changeCommand = lambda x : 'cmds.ligneColonnes(nombre = 5, hauteur = 10, espacement = ' + float(x) + ')')
# des float sont utilisés pour la hauteur et l'espacement
# penser à changer le type de contrôle et le type de la variable de lambda

Cela contrôle bien chaque paramètre, mais sans “sauvegarder” la modification faite sur les autres. Normal : dans mes lignes de commande les autres paramètres que celui contrôlé sont statiques.

On se retrouve donc bloqué lorsqu'il y a plusieurs arguments à faire varier1). Il faut alors changer de technique.

Une fonction dont on n'a besoin de faire varier qu'un seul paramètre, par exemple “creerLumieres(nombre)”, “supprimerObjet(nom)”, etc… peut être contrôlée de cette manière, c'est court et efficace.

Contrôle total

Bon, faisons un point : .

Voila, continuons.

Nous savons faire une fonction, nous savons l'appeler depuis nos contrôleurs, mais nous avons besoin de récupérer la valeur de nos contrôleurs. Et là, magique, on trouve une utilité au fameux query=True que l'on peut mettre un peu partout. Nous avons besoin de leur valeur? Demandons-leur!

Pour leur demander, il faut savoir qui appeler. Donnons donc un nom à nos contrôleurs :

cmds.intSliderGrp(  'nombre',     label = 'nombre',     value = 5,  field = True)
cmds.floatSliderGrp('hauteur',    label = 'hauteur',    value = 10, field = True )
cmds.floatSliderGrp('espacement', label = 'espacement', value = 2,  field = True )
# ne pas confondre leur nom et leur label : le label est seulement le texte affiché devant le controleur
# j'ai rajouté la valeur par défaut et le field

Maintenant demandons-leurs quelle est leur valeur et stockons ça dans des variables :

nombre     = cmds.intSliderGrp(  'nombre',     query = True, value = True)
hauteur    = cmds.floatSliderGrp('hauteur',    query = True, value = True)
espacement = cmds.floatSliderGrp('espacement', query = True, value = True)

Ainsi nous n'avons plus qu'à rapeller notre fonction ligneColonnes() avec ces nouveaux paramètres :

ligneColonnes(nombre, hauteur, espacement)
# pas la peine de mettre nombre=nombre, etc... si l'on voit bien qui est qui

Oui, mais attends, comment je fais tout ça quand je bouge mon slider ? Avec une fonction, encore !

On met tout ça dans une fonction…

def update() :
     '''lie la fonction ligneColonnes et ses controleurs'''
     # demande la valeur des sliders et les stocke dans des variables
     nombre     = cmds.intSliderGrp(  'nombre',     query = True, value = True)
     hauteur    = cmds.floatSliderGrp('hauteur',    query = True, value = True)
     espacement = cmds.floatSliderGrp('espacement', query = True, value = True)
     # appelle la fonction ligneColonnes avec les valeurs précédentes
     ligneColonnes(nombre, hauteur, espacement)

…et nous n'avons plus qu'à dire que lorsqu'on modifie un des contrôleurs on appelle notre fonction update() :

cmds.intSliderGrp(  'nombre',     label = 'nombre',     changeCommand = "update()", value = 5,  field = True)
cmds.floatSliderGrp('hauteur',    label = 'hauteur',    changeCommand = "update()", value = 10, field = True )
cmds.floatSliderGrp('espacement', label = 'espacement', changeCommand = "update()", value = 2,  field = True )

Ainsi quand on modifie un contrôleur on n'apelle pas directement notre fonction principale mais notre fonction d'update, qui se charge de mettre à jour. Pourquoi avoir pris ce chemin détourné au lieu de mettre ça directement dans ligneColonnes? Imaginez, par exemple, que vous sachiez faire de l'aléatoire. Vous pourriez tout à fait faire une fonction “randomUpdate()” qui appellerais votre fonction avec des valeurs aléatoires. Cloisonner chaque action permet de rajouter des fonctionnalités facilement, sans modifier vos fonctions existantes.

En tout cas, notre fonction ligneColonnes() est entièrement contrôlable !

Bien entendu vous pouvez adapter tout ça avec les contrôleurs que vous voulez (boutons, boites à cocher, listes déroulantes…) et à la fonction que vous voulez (tomate(), lampadaire(), temple()…)

Le script complet

Le code est modifié afin de se nettoyer proprement. Voir Nettoyer sa scène, supprimer ses objets.

import maya.cmds as cmds
 
#fonction de nettoyage
def deleteGroup(name) :
	'''Supprime un groupe'''
	if cmds.objExists(name) :
		cmds.delete(name)
 
# fonction principale
def ligneColonnes(nombre, hauteur, espacement) :
	'''Cree x colonnes cotes à cotes, de hauteur et espacement donnes'''
	for i in range(nombre):
		cmds.polyCube(n = 'tomate_colonne', h = hauteur) # cree un cube de hauteur donnee
		cmds.move(i*espacement,0,0) # le deplace selon l'espacement donne
	cmds.group('tomate_*', name = 'tomateGroup') # on met tout dans un groupe
 
# fonction de mise a jour
def update() :
	'''lie la fonction ligneColonnes et ses controleurs'''
	# on demande la valeur des sliders et on les stocke dans des variables
	nombre     = cmds.intSliderGrp(  'nombre',     query = True, value = True)
	hauteur    = cmds.floatSliderGrp('hauteur',    query = True, value = True)
	espacement = cmds.floatSliderGrp('espacement', query = True, value = True)
 
	# on supprime l'ancien groupe
	deleteGroup('tomateGroup')
	# on appelle la fonction ligneColonnes avec les valeurs précédentes
	ligneColonnes(nombre, hauteur, espacement)
 
# suppression du groupe au lancement du script
deleteGroup('tomateGroup')
 
# execution de la fonction au lancement du script
ligneColonnes(nombre = 5, hauteur = 10, espacement = 2)
 
#affichage de la fenetre de controle
cmds.window()
cmds.columnLayout()
cmds.intSliderGrp(  'nombre',     label = 'nombre',     changeCommand = "update()", value = 5,  field = True)
cmds.floatSliderGrp('hauteur',    label = 'hauteur',    changeCommand = "update()", value = 10, field = True )
cmds.floatSliderGrp('espacement', label = 'espacement', changeCommand = "update()", value = 2,  field = True )
cmds.showWindow()

Si vous avez des questions, des remarques, des suggestions, n'hésitez pas à m'en faire part.

1)
c'est en réalité possible au moyen de variables globales, mais tout à fait contre-indiqué

Discussion

Entrer votre commentaire. La syntaxe wiki est autorisée: