Projeter un mesh sur un autre avec l'API Python de Maya
Par Narann le dimanche, 20 février 2011, 23:56 - Script et code - Lien permanent
Aujourd'hui je vous propose un tutorial qui vous permettra de projeter un mesh sur un autre.
C'est un truc qui peut être pratique mais surtout, c'est assez "fun" à faire (tout est relatif diront certains :seSentCon: ) et ça permet d'apprendre les fondamentales des changements de repères (les fameuses matrices) en ayant un exemple à la fois simple et concret.
Amateur de l'API, ce tuto est fait pour vous!
Voici ce que nous souhaitons obtenir:
Un mesh projeté sur un autre.
Et la scène à partir de laquelle nous partons:
Sommaire
- Théorie
- Code de base
- Préparation des attributs
- Préparer sa scene
- La méthode compute
- Les matrices expliquées aux graphistes
- Revenons au code!
- Parcourir et modifier chaque vertex
- Construire un mesh
- Conclusion et code source
- Mise à jour: La méthode sans Python
Théorie
Une fois de plus on commence par de la théorie.
Dans les faits, vous allez voir que c'est assez simple car le plus dur (la partie intersection) sera géré via un appel à l'API Maya:
OpenMaya.MFnMesh.closestIntersection( ... )
Cette méthode prend en charge l'intersection d'un rayon (point+direction) sur un mesh et renvoi quelques infos (dont la plus importante: La position du point projeté).
En gros, il nous faut trois choses:
- Un point d'origine (celui qu'on souhaite projeter).
- Une direction (un vecteur).
- Un mesh de destination.
Le point d'origine sera bien entendu chaque vertex du mesh à projeter (ici, les numéros des vertex étant en jaune).
La direction sera la normale du vertex à projeter (en vert sur l'image. D'accord on voit pas trop mais mettez y un peu de bonne volonté que diable! :cayMal: ).
Et le mesh de destination sera bien évidemment le mesh qui recevra le plan (dans notre cas, une sphere).
On récupère donc, à chaque fois, un point et une normale (les croix jaunes).
Code de base
Voici les bases du code:
import sys import maya.OpenMaya as OpenMaya import maya.OpenMayaMPx as OpenMayaMPx kPluginNodeTypeName = "projectMesh" kPluginNodeId = OpenMaya.MTypeId( 0x80000 ) # Node definition class projectMeshNode( OpenMayaMPx.MPxNode ) : # constructor def __init__( self ) : OpenMayaMPx.MPxNode.__init__( self ) def compute( self, plug, data ) : print "compute" return OpenMaya.MStatus.kSuccess # creator def nodeCreator(): return OpenMayaMPx.asMPxPtr( projectMeshNode() ) # initializer def nodeInitializer() : return # initialize the script plug-in def initializePlugin( mobject ) : mplugin = OpenMayaMPx.MFnPlugin( mobject ) try: mplugin.registerNode( kPluginNodeTypeName, kPluginNodeId, nodeCreator, nodeInitializer ) except: sys.stderr.write( "Failed to register node: %s\n" % kPluginNodeTypeName ) raise # uninitialize the script plug-in def uninitializePlugin( mobject ): mplugin = OpenMayaMPx.MFnPlugin( mobject ) try: mplugin.deregisterNode( kPluginNodeId ) except: sys.stderr.write( "Failed to unregister node: %s\n" % kPluginNodeTypeName ) raise
Si vous avez déjà écrit un node Maya en Python, ce code ne doit pas vous faire trop peur.
J'explique vite fait pour les boulets autres :baffed: .
import sys import maya.OpenMaya as OpenMaya import maya.OpenMayaMPx as OpenMayaMPx
Import des principaux modules.
- Le module sys sert à créer les messages d'erreur lors du chargement/déchargement du plugin (voir plus loin).
- Les deux autres modules servent à appeler les méthodes de l'API Maya.
Rien de bien compliqué.
kPluginNodeTypeName = "projectMesh" kPluginNodeId = OpenMaya.MTypeId( 0x80000 )
- kPluginNodeTypeName est une simple variable appelé plus loin pour donner un nom à notre type de node.
- kPluginNodeId est une valeur qui sert d'identifiant pour le node quand il est écrit dans dans les fichiers binaires (mb). 0x80000 à 0xfffff sont utilisé pour les examples Maya. Voir la documentation pour plus d'informations.
# Node definition class projectMeshNode( OpenMayaMPx.MPxNode ) : # constructor def __init__( self ) : OpenMayaMPx.MPxNode.__init__( self ) def compute( self, plug, data ) : print "compute" return OpenMaya.MStatus.kSuccess
Ici, la classe est une instance de l'objet MPxNode qui est lui même une classe faite pour créer des nodes personnalisé (c'est une partie assez complexe que je n'aborderai pas dans ce tuto tant elle mérite un billet à part entière :jdicajdirien: ).
La méthode __init__ est la première méthode lancée lors de la création de la classe. Elle initialise simplement la classe MPxNode.
La méthode compute est la méthode dans laquelle nous allons le plus travailler. C'est une méthode hérité de MPxNode. La partie du code "qui fait quelque chose". :sourit:
Si on ne connaît pas trop Python et les classes, cette partie du code peut sembler complexe mais ne vous inquiétez pas, c'est toujours la même qu'on utilise. La seule partie importante, c'est la méthode compute.
# creator def nodeCreator(): return OpenMayaMPx.asMPxPtr( projectMeshNode() )
Une fonction lancée au moment de l'initialisation du plugin qui renvoi un pointeur (si si) vers la classe (et donc le node) crée.
Python n'ayant pas de notions de pointeur et Maya en ayant besoin, notamment, pour initialiser ces plugins, Autodesk a créé la méthode OpenMayaMPx.asMPxPtr (rechercher "asMPxPtr" dans l'aide Maya et prendre le premier résultat pour une explication plus précise).
Une fois de plus, c'est quelque chose de basique, on le met, on réfléchie pas :bete: .
# initializer def nodeInitializer() : return
Cette méthode est elle aussi appelé lors de la création d'un node et permet (entre autres) d'initialiser les attributs du node. Ce sera la première que nous remplirons. Pour l'instant, elle en fait rien.
# initialize the script plug-in def initializePlugin( mobject ) : mplugin = OpenMayaMPx.MFnPlugin( mobject ) try: mplugin.registerNode( kPluginNodeTypeName, kPluginNodeId, nodeCreator, nodeInitializer ) except: sys.stderr.write( "Failed to register node: %s\n" % kPluginNodeTypeName ) raise # uninitialize the script plug-in def uninitializePlugin( mobject ): mplugin = OpenMayaMPx.MFnPlugin( mobject ) try: mplugin.deregisterNode( kPluginNodeId ) except: sys.stderr.write( "Failed to unregister node: %s\n" % kPluginNodeTypeName ) raise
La je vais vous dire, c'est vraiment un "copier-coller" que je fais depuis les exemples fournit. En gros ces fonctions sont appelé lors du chargement/déchargement des plugins. Elles servent à "enregistrer"/"radier" les plugins des sessions Maya.
Leur comportement est simple, je vous invite à analyser ce bout de code par vous même (ça fait pas de mal! :gniarkgniark: ).
A ce stade, vous devriez pouvoir créer votre node python en le chargeant dans Maya:
Et en le créant comme suis:
createNode projectMesh; // Result: projectMesh1 //
Dans la mesure ou il n'y a aucun attribut, ce node ne fait absolument rien! :sourit:
Préparation des attributs
Comme promis on va commencer par la méthode nodeInitializer() qui initialiser les attributs du node.
Nous allons avoir besoin de trois attributs:
- Deux en entrée (input): Le mesh qui projette ses vertex et celui qui les reçoit.
- Un en sortie (output): Le mesh de sortie, le mesh projeté.
C'est partie! :grenadelauncher:
# initializer def nodeInitializer() : typedAttr = OpenMaya.MFnTypedAttribute() # Setup the input attributes projectMeshNode.inputMeshSrc = typedAttr.create( "inputMeshSrc", "inMeshSrc", OpenMaya.MFnData.kMesh ) typedAttr.setReadable(False) projectMeshNode.inputMeshTarget = typedAttr.create( "inputMeshTarget", "inMeshTrg", OpenMaya.MFnData.kMesh ) typedAttr.setReadable(False) # Setup the output attributes projectMeshNode.outputMesh = typedAttr.create( "outputMesh", "outMesh", OpenMaya.MFnData.kMesh ) typedAttr.setWritable(False) typedAttr.setStorable(False) # Add the attributes to the node projectMeshNode.addAttribute( projectMeshNode.inputMeshSrc ) projectMeshNode.addAttribute( projectMeshNode.inputMeshTarget ) projectMeshNode.addAttribute( projectMeshNode.outputMesh ) # Set the attribute dependencies projectMeshNode.attributeAffects( projectMeshNode.inputMeshSrc, projectMeshNode.outputMesh ) projectMeshNode.attributeAffects( projectMeshNode.inputMeshTarget, projectMeshNode.outputMesh )
La première ligne:
typedAttr = OpenMaya.MFnTypedAttribute()
Crée un "objet" (appelé dans l'API Maya: "Function Set") qui va nous servir à manipuler les attributs (les créer surtout):
# Setup the input attributes projectMeshNode.inputMeshSrc = typedAttr.create( "inputMeshSrc", "inMeshSrc", OpenMaya.MFnData.kMesh ) typedAttr.setReadable(False) projectMeshNode.inputMeshTarget = typedAttr.create( "inputMeshTarget", "inMeshTrg", OpenMaya.MFnData.kMesh ) typedAttr.setReadable(False) # Setup the output attributes projectMeshNode.outputMesh = typedAttr.create( "outputMesh", "outMesh", OpenMaya.MFnData.kMesh ) typedAttr.setWritable(False) typedAttr.setStorable(False)
On créé les attributs. Rien de bien compliqué (voir doc).
Quelques précisions sur les arguments utilisés:
- Le nom entier de l'attribut (long name).
- Le nom court de l'attribut (short name).
- Le "type" (au sens API type) de l'attribut.
Les méthodes qui suivent chaque déclaration d'attribut (setReadable, setWritable, setStorable) rajoutent des particularitées au dernier attribut créé (voir la doc pour plus de précisions).
# Add the attributes to the node projectMeshNode.addAttribute( projectMeshNode.inputMeshSrc ) projectMeshNode.addAttribute( projectMeshNode.inputMeshTarget ) projectMeshNode.addAttribute( projectMeshNode.outputMesh )
Comme le commentaire l'indique, cette partie ajoute/connecte les attributs créés plus haut au node.
# Set the attribute dependencies projectMeshNode.attributeAffects( projectMeshNode.inputMeshSrc, projectMeshNode.outputMesh ) projectMeshNode.attributeAffects( projectMeshNode.inputMeshTarget, projectMeshNode.outputMesh )
Cette partie est très importante! :papi:
Elle permet de définir des "dépendances" entre les attributs.
Dans notre cas:
- Si l'attribut inputMeshSrc change, l'attribut outputMesh changera aussi.
- Si l'attribut inputMeshTarget change, l'attribut outputMesh changera aussi.
Si ces lignes ne sont pas mises, la méthode compute du node que nous sommes en train de créer ne sera jamais lancé. Le node ne sera donc jamais mis à jour.
Vous pouvez télécharger le node python en l'état actuel ici:
Préparer sa scene
Avant de réellement coder le comportement du node, il nous faut de la géométrie déjà présentes dans la scène à laquelle connecter notre futur node.
Créer une scène qui ressemble à ça:
Créez aussi un troisième mesh, celui qui renverra la géométrie de notre futur node (qui sera le mesh projeté).
Dans mon cas: Une pSphere. Mais ça peut être n'importe quoi.
Tant que c'est un node de mesh. Vous pouvez même créer le node de mesh à la main.
Placez le au centre de la scène (0,0,0) pour qu'il n'y ai pas de décalage entre le mesh projeté et sa cible.
Pour éviter d'avoir à créer les connections à chaque fois, voici deux petits codes MEL qui permettent de tester votre node (il faut que vos nodes soit bien nommés).
Pour charger le tout:
loadPlugin( "monRepertoire/projectMesh.py" ); createNode projectMesh; connectAttr -f projectMesh1.outputMesh pSphereShape2.inMesh; connectAttr -f pPlaneShape1.worldMesh[0] projectMesh1.inputMeshSrc; connectAttr -f pSphereShape1.worldMesh[0] projectMesh1.inputMeshTarget;
Et pour tout décharger:
delete projectMesh1; flushUndo; unloadPlugin( "projectMesh" );
Et voila le travail! Maintenant prenez une grosse inspiration, on saute!
La méthode compute
La première chose à tester est la présence d'une connexion sur votre attribut outputMesh. En effet, si votre node n'est connecté à rien, il ne faut pas qu'il se calcule:
def compute( self, plug, data ) : if plug == self.outputMesh: print "compute" else: return OpenMaya.MStatus.kUnknownParameter return OpenMaya.MStatus.kSuccess
Une fois qu'on est sûr que les connections sont bonnes, on récupère les attributs d'entrés:
if plug == self.outputMesh: # get the inputMeshTarget (return MDataHandle) inMeshSrcHandle = data.inputValue( self.inputMeshSrc ) inMeshTargetHandle = data.inputValue( self.inputMeshTarget )
Ceci fait une "connection" vers le bloc de donnée des attributs. C'est la première étape pour récupérer la valeur (ici c'est un kMesh donc ce sera un peu différent) d'un attribut.
Python étant un langage non typé (A la fois sa principale qualité mais aussi son principal défaut...), j'ai tendance à écrire en commentaire le type des données de l'API Maya que je récupère.
Sinon, on a (très) vite fait de ne plus savoir du tout quelle variable correspond à quel type (surtout que des types dans l'API Maya, c'est pas ça qui manque :aupoil: ).
Après, chacun sa méthode! Si vous avez un cortex sur développé, que vous voulez vous la jouer "Reunabranlé moi j'type queudal", qu'un code que vous êtes le seul à pouvoir lire vous met le kiki tout dur et que vous voulez justifier votre BAC+5 (par les temps qui courent ce n'est sûrement pas votre salaire qui doit s'en charger). N'hésitez pas, codez comme un
TDporc: Pas de commentaires, des variables à une lettre et autres joyeusetés du genre... Vos collègues vous le rendront bien. :sourit:Mais si vous êtes plus modeste et souhaitez apprendre rapidement l'API Maya, je vous recommande vivement d'écrire les types de l'API Maya via des commentaires, directement dans votre code. En plus d'être plus clair, ça oblige à toujours savoir/chercher, quand on écrit des variables, à quel type elle correspond.
Après ça, nous vérifions que nos deux attributs connectés sont bien des meshs:
#we check the API type we've got here (we need kMesh) if inMeshSrcHandle.type() == OpenMaya.MFnData.kMesh and inMeshTargetHandle.type() == OpenMaya.MFnData.kMesh : print "cool" else: return OpenMaya.MStatus.kInvalidParameter
Et nous les récupèrons en tant que tel:
#we check the API type we've got here (we need kMesh) if inMeshSrcHandle.type() == OpenMaya.MFnData.kMesh and inMeshTargetHandle.type() == OpenMaya.MFnData.kMesh : # return a MObject meshSrc = inMeshSrcHandle.asMesh() meshTarget = inMeshTargetHandle.asMesh() print "cool" else: return OpenMaya.MStatus.kInvalidParameter
Je vous invite à regarder la doc de MDataHandle histoire de voir ce qu'on peut récupérer d'un attribut.
Comme précisé dans le commentaire, on récupère un MObject. Ce type d'objet un peu "le type à tout faire" dans Maya.
Ce MObject n'est qu'un objet de transition. En effet, il est rarement utilisé directement.
Dans Maya, pour modifier/manipuler des objets, on passe souvent par des "Function Set". Ils ont la forme: MFn*Type*.
Ici, pour manipuler les mesh, on va récupérer un function set de mesh: MFnMesh
# get the MFnMesh of the twice attr mFnMeshSrc = OpenMaya.MFnMesh( meshSrc ) mFnMeshTarget = OpenMaya.MFnMesh( meshTarget )
Ce qui nous intéresse maintenant c'est d'avoir une liste de tout les vertex du "mesh source" (celui qui va être projeté sur le "mesh cible") afin de créer un autre tableau de vertex qui contiendra leurs positions modifié:
outMeshMPointArray = OpenMaya.MPointArray() # create an array of vertex wich will contain the outputMesh vertex mFnMeshSrc.getPoints(outMeshMPointArray) # get the point in the space
La première ligne créée un tableau de type MPointArray.
La seconde ligne le remplit avec les valeurs des vertex du "mesh source".
La façon de l'écrire est un peu déroutante ("à l'envers" diront certains :reflechi: ) mais c'est comme ça que fonctionne getPoint comme pas mal d'autres fonctions de l'API Maya.
Plutôt que de renvoyer le résultat, il est stocké dans la variable fournit en argument.
Nous avons maintenant un MPointArray remplit de vertex avec leurs positions.
L'idée est maintenant de modifier la position de ces vertex afin qu'elles correspondent à la position projetée sur le "mesh cible".
Mais voila... Les positions des vertex que vous avez récupéré dans votre MPointArray sont en "object space". C'est-à-dire, relatif au centre de l'objet.
Nous allons nous heurter à un vrai problème. The big one! The ultimate: The matrices! *Voix qui résonne* :enerve:
Les matrices expliquées aux graphistes
Quand on est graphistes, on en entend des fois parler sans trop savoir ce que c'est :bete: .
Ajoutez à ça que ce qu'on trouve sur le net est très scolaire et "trop mathématique" (On montre comment multiplier une matrice sans expliquer pourquoi on est amené à le faire).
Tout ça au point qu'on ne voit pas forcément le lien avec notre boulot.
Je vais modestement tenter d'expliquer ça d'un point de vue "graphiste" :mayaProf: .
Créez un cube.
Vous avez sûrement remarqué, une fois votre cube créé, qu'il a un "point de pivot" avec des informations (position, rotation, échelle, etc...). Et bien ce point permet de faire un "lien mathématique" entre les points de vertex de votre cube et "le monde" (les coordonnées centrales du "monde" sont 0,0,0).
Dans l'idée, si ce node (le node de "transform") n'existait pas, votre objet serait au centre de la scène. Et pour le déplacer il faudrait déplacer les positions de tout les vertex du cube.
Le node de transform agit un peu comme un "parent" des vertex de votre cube. De cette façon, les positions des vertex du cube ne bouge pas. Par exemple, un vertex du cube placé à 1,1,1 (par rapport au pivot du cube donc) restera à 1,1,1 quel que soit la position du transform.
Mais pour pouvoir faire certaines opérations (dans notre cas, savoir si le vertex est dirigé vers un autre mesh), il faut que les coordonnées de toute les entités qui entrent en jeu soient sur un repère commun, le repère monde.
Démonstration en bédé:
Alors que si on choisi, comme point de repère commun, le centre du monde.
C'est beaucoup plus facile.
Bon, maintenant qu'on connaît les coordonnées des vertex dans leurs "espaces objets", il faut savoir comment les récupérer en "espace monde".
Le principe de base qui vient tout de suite à l'esprit est: On additionne les positions (relatives à l'objet) des vertex à la position (relative au monde) de son point de pivot.
Exemple:
Si pVertex la position d'un vertex et positionDuCube la position, dans le monde, du pivot du cube:
positionDuCubeX + pVertexDansLeCubeX = pVertexDansLeMondeX
Mais vous vous en doutez surement, c'est plus compliqué... :siffle:
En effet, dans le cas des rotations et de l'échelle, il ne suffit pas de quelques additions pour résoudre le problème.
Note: Que ce soit une transformation, une rotation, ou une mise à l'échelle d'un mesh. Tout se résume à un déplacement des vertex dans l'espace.
La vérité est que tout ses "paramètres" peuvent être mis dans un seul et même "objet" que l'on appel une matrice. Cette matrice, va nous servir à faire des calcules (que Maya nous épargne mais si ça vous intéresse, voici un exemple de calcule d'une matrice de rotation) pour récupérer les positions des vertex dans l'espace monde.
Comme je disait, Maya nous donne très facilement accès à cet objet grâce à l'inclusiveMatrix (Il y a plusieurs types de matrices, nous nous focaliserons que sur celle là).
Du coup, à ce stade, nous avons deux choses:
- Les positions des vertex relativement à l'objet (en "object space").
- Une matrice de l'objet (qui est relative au monde, en "world space").
Il faut donc "convertir" les positions des vertex de "object space" vers "world space". On Parle d'un changement de repère. Vous obtenez donc une position dite absolue ("world space").
Et pour obtenir la position d'un vertex dans le "world space", il "suffit" de multiplier la matrice de position d'un vertex {x,y,z} par l'inclusive matrix de l'objet. (J'ai mis "suffit" entre guillemet car multiplier une matrice c'est pas aussi simple que faire 2x2... :redface: )
Note: Je ne ferais pas de démonstration sur "comment calculer une matrice", le net regorgeant d'exemples et d'explications.
Et voici la formule:
positionGlobale = positionLocale * inclusiveMatrix
C'est un peu comme le théorème de Pythagore. On s'en fout de comment ça marche, tant qu'on sait quand l'utiliser! :baffed:
Voila! J'espère que cette petite explication vous aura éclairé un peu sur le pourquoi des matrices. :sourit:
Revenons au code!
Pour récupérer l'inclusive matrice, rien de plus simple.
# get MDagPath of the MMesh to get the matrix and multiply vertex to it. If I don't do that, all combined mesh will go to the origin inMeshSrcMDagPath = mFnMeshSrc.dagPath() # return MDagPath object inMeshSrcInclusiveMMatrix = inMeshSrcMDagPath.inclusiveMatrix() # return MMatrix
La première ligne permet de récupérer le dagPath du mesh source. On peut considérer le dagPath comme étant l'équivalent du node de transform d'un objet dans Maya. Là ou toutes les informations sur les transformations (positions, rotations, échelles, etc...) sont stocké.
La seconde ligne récupère l'inclusiveMatrix du mesh source sous forme d'une MMatrix.
Maintenant, nous allons pouvoir parcourir chaque vertex:
- Le multiplier par la matrice afin d'avoir sa position dans le world space.
- Récupérer sa normale.
- La mutiplier elle aussi par la matrice.
- Récupérer le point de collision, le stocker à la place du point en court.
Parcourir et modifier chaque vertex
Le début de la boucle principale ressemble à ça:
for i in range( outMeshMPointArray.length() ) : inMeshMPointTmp = outMeshMPointArray[i] * inMeshSrcInclusiveMMatrix # the MPoint of the meshSrc in the worldspace
La boucle est simple: "i" sera incrémenté de 1 à chaque "tour" pour parcourir le tableau de vertex (MPointArray).
La première chose qu'on fait est une multiplication du point (outMeshMPointArray[i]) par la matrice (inMeshSrcInclusiveMMatrix) pour obtenir un vertex (inMesgPointTmp) en world space (avec des coordonnées relatives au "monde").
Maintenant que nous avons (enfin) le vertex positionné par rapport au monde, on va "l'intersectionner" (si les gars de l'académie française voyait ça :pasClasse: ) avec le mesh cible.
Bon, allez regarder les arguments de la méthode OpenMaya.MFnMesh.closestIntersection() que nous avont vu plus haut.
Vous voyez qu'il y en a pas mal. :sourit:
Rassurez vous, nous pouvons en faire sauter la plupart. Ce qui nous intéresse c'est le vertex d'origine, sa direction (dans notre cas: la normale) et le point de "collision" (le hitPoint).
Mais plus subtile encore, regardez le type du premier argument attendu par la méthode (le raySource).
C'est un MFloatPoint!
Mais comment convertir inMeshMPointTmp (un MPoint) en MFloatPoint? En C++ c'est assez facile, il faut passer par des doubles. Après moult recherches, j'ai trouvé une solution. Je vous la donne de but en blanc:
raySource = OpenMaya.MFloatPoint( inMeshMPointTmp.x, inMeshMPointTmp.y, inMeshMPointTmp.z )
C'est pas si compliqué mais si tu le sais pas...
Nous avons donc notre raySource.
Maintenant la direction:
rayDirection = OpenMaya.MVector() mFnMeshSrc.getVertexNormal( i, False, rayDirection) rayDirection *= inMeshSrcInclusiveMMatrix rayDirection = OpenMaya.MFloatVector(rayDirection.x, rayDirection.y, rayDirection.z)
Arrivé ici vous devriez comprendre:
- On créé le MVector.
- On y stock la normale du vertex courant ("i") du "mesh source".
- On multiplie par la matrice pour avoir ce vecteur relatif à l'espace monde.
- On le "convertie" en MFloatVector.
Le hitPoint quand à lui est un simple MFloatPoint:
hitPoint = OpenMaya.MFloatPoint()
Et le reste des arguments sont les suivants:
# rest of the args hitFacePtr = OpenMaya.MScriptUtil().asIntPtr() idsSorted = False testBothDirections = False faceIds = None triIds = None accelParams = None hitRayParam = None hitTriangle = None hitBary1 = None hitBary2 = None maxParamPtr = 99999999 # http://zoomy.net/2009/07/31/fastidious-python-shrub/ hit = mFnMeshTarget.closestIntersection(raySource, rayDirection, faceIds, triIds, idsSorted, OpenMaya.MSpace.kWorld, maxParamPtr, testBothDirections, accelParams, hitPoint, hitRayParam, hitFacePtr, hitTriangle, hitBary1, hitBary2)
Un grand merci à Peter J. Richardson! Sans son billet, je n'aurais jamais réussi à coder ce truc. C'est pour ça qu'il faut "partager ce qu'on sait" sur internet! ;)
Une fois le closestIntersection appelé, vous récupérez un hitPoint en MFloatPoint que vous reconvertissez en MPoint:
if hit : inMeshMPointTmp = OpenMaya.MPoint( hitPoint.x, hitPoint.y, hitPoint.z)
On remplace le point courant par notre nouveau point:
outMeshMPointArray.set( inMeshMPointTmp, i )
Et c'est la fin de la boucle! :D
Arrivé ici vous avez un MPointArray outMeshMPointArray avec les valeurs des vertex projetés sur le mesh cible.
Il faut donc maintenant reconstruire le mesh.
Construire un mesh
Je ne vais pas rentrer précisément dans les détails sur "comment créer et agencer les variables dans le cas de la création d'un mesh.
En gros il nous faut:
- Le nombre de vertex.
- Le nombre de polygones (polygones + triangles si il y en a).
- Un tableau de point (Tout les vertex à la queue leu leu avec leurs coordonnées XYZ).
- Un tableau listant le nombre de vertex par polygones (Exemple: 4,4,4,4,3,4,3,4,3,4,4,etc...)
- Un tableau d'index des vertex (Exemple: 1,2,3,4,3,4,5,6,5,6,7,8, etc...)
Vous l'aurez compris, on déjà le tableau des points (le troisième point). Pour le reste, on le récupère bêtement sur le mesh d'origine.
Commençons! :hehe:
Dans un premier temps il faut créer un function set MFnMeshData.
# create a mesh that we will feed! newDataCreator = OpenMaya.MFnMeshData()
Ce function set va nous permettre de créer un MObject que nous allons pouvoir "remplir" des données du futur mesh.
newOutputData = newDataCreator.create() # Return MObject
Comme je vous l'ai dit plus haut: Il faut qu'on récupère toute les informations (hormis le tableau de vertex) du mesh source:
outMeshNumVtx = mFnMeshSrc.numVertices() # outputMesh will have the same number of vtx and polygons outMeshNumPolygons = mFnMeshSrc.numPolygons() # create two array and feed them outMeshPolygonCountArray = OpenMaya.MIntArray() outMeshVtxArray = OpenMaya.MIntArray() mFnMeshSrc.getVertices(outMeshPolygonCountArray, outMeshVtxArray)
mFnMeshSrc.getVertices() remplit les deux tableaux avec les informations nécessaires à la création du mesh voir plus haut
Une fois que nous avons tout ça, nous créons le mesh:
meshFS = OpenMaya.MFnMesh() meshFS.create(outMeshNumVtx, outMeshNumPolygons, outMeshMPointArray, outMeshPolygonCountArray, outMeshVtxArray, newOutputData)
Le principe est assez simple:
- On créer un function set MFnMesh
- On créer le mesh en donnant tout les arguments (récupéré plus haut) via meshFS.create().
Une fois que le mesh est créé, on récupère le MDataHandle de la connexion "outputMesh " pour y "mettre" le MObject que l'on vient de remplir: Le mesh!
# Store them on the output plugs outputMeshHandle = data.outputValue( self.outputMesh ) outputMeshHandle.setMObject( newOutputData )
Une fois cela fait, on dit au dependency graph, via MDataBlock.setClean(), que la connexion a été mise à jour.
# tell to the dependency graph the connection is clean data.setClean( plug )
Et c'est fini! :youplaBoum:
Si vous avez bien suivi le tuto (et si je ne me suis pas planté ( :baffed: ), vous devriez avoir un node qui marche correctement (placez le plan de sorte qu'il "vise" la sphère):
Bien sur, si les vertex ne sont pas projeté sur la sphère, il retourne à leur position d'origine:
Conclusion et code source
Arrivez ici, vous devriez avoir compris le principe des matrices (Si ce n'est pas le cas, n'hésitez pas à approfondir, vous verrez la 3D d'une autre façon! :sourit: ), être capable de récupérer les composants d'un mesh et de créer un mesh à partir de rien.
Voici le code source: projectMesh.7z
Et voilà! Je viens de finir un autre gros tuto. J'espère qu'il aura été instructif et que vous allez pouvoir commencer à faire des choses intéressantes avec l'API Maya. :banaeyouhou:
Ce genre d'informations manquent sur l'internet de l'infographie francophone. :franceHappy:
J'invite donc les seniors qui passeraient par là et qui tireraient quelque chose d'intéressant de ce qu'ils ont lu de ne pas hésiter à "rendre la pareille": Si vous êtes compétent dans un domaine, partagez! Je l'ai fait. :hihi:
N'hésitez pas à me dire si je me suis trompé quelque part, si un point ne vous semble pas clair ou si il y a une erreur. :pasClasse:
A bientôt!
Mise à jour: La méthode sans Python
Bon, ce n'est pas vraiment le but mais puisque Kel Solar en parle dans les commentaires je vous donne un moyen de faire ça en utilisant la méthode intégré dans Maya. :seSentCon:
Sélectionner le mesh cible:
Sélectionner le mesh source:
Ouvrez les options du transfert d'attributs:
Settez les options comme suis:
En gros, on ne transfert que la position des vertex dans l'espace monde en les projetant le long de leur normale.
Et voilà le travail!
Vous pouvez tourner le mesh source et c'est actualisé directement! :laClasse:
Voilà! Comme ça, les personnes qui sont venu pour trouver une solution rapide ne seront pas frustré! :sourit:
Commentaires
Excellent ! J'ai vite parcouru le post, çà à l'air bien expliqué ( à part certaines images douteuses farçies de mots doux ). Pour information Maya propose un outil équivalent : Mesh - > Transfer Attributes.
Thomas
lol Tu me blase Thomas...
En effet, le transfert d'attributs fait exactement ça... Je vais l'ajouter à la fin du tutorial.
Bon, je me rassure (comme je peut) en me disant que l'idée ici est surtout d'apprendre à utiliser l'API Python de Maya...
Merci quand même (ouai, tu m'as quand même bien pourris ma journée là. ^^ )
EDIT: Je me permet d'ajouter ton magnifique portfolio à ton pseudo. :D
Excellent tutorial comme toujours
très instructif merci !
Tres interessant , un grand merci
Bonsoir Dorian ^^
Et oui c'est encore moi =) Sur tes conseil je me suis retroussé les manches et j'ai plongé les main ds python :D et je dois dire que j'aime bien ce que je découvre petit a petit ^^
En fait je regardais tn code ici présent et je me posais une question ! Après avoir récupéré la position de mes vertex dans mon " World Space " comment je fais pour afficher celle ci, en faire une liste quoi ?
je me suis donc arrété a un bout du code :
inMeshSrcMDagPath = mFnMeshSrc.dagPath()
inMeshSrcInclusiveMMatrix = inMeshSrcMDagPath.inclusiveMatrix()
for i in range( outMeshMPointArray.length() )
inMeshMPointTmp = outMeshMPointArray[i] * inMeshSrcInclusiveMMatrix
je me demandais donc si un simple print "inMeshMPointTmp" suffisait a afficher la liste de toute les position des vertex ? Oo
Mon idée c'est de pouvoir récupérer les position de ces vertex et y assignez un objet par exemple ( Lights,émetteur de particules,...) je sais pas trop si tu voix ce qu je veux dire :s
C'est peut être bête pour certains ><' ....mais en tant que débutant je me dis que ça pourrait être fun comme exercice ^^'
Dc si ta une idée et du temps pour répondre, ben ce serait cool que tu m'explique ça si possible
Merci a toi =)
LilCed.
@zapan669:
De rien! ;)
@LilCed:
Alors alors... Ici il faut savoir que c'est l'API Maya qu'on utilise et non plus simplement les commandes Maya (MEL) donc la façon de faire ressemble beaucoup plus à de la programmation qu'à du script. :)
Mais c'est une très bonne chose de connaitre l'API. :hehe:
La variable "inMeshMPointTmp" est un object MPoint.
Faire directement un print de ce type de variable ne te donnera rien. :)
Mais un MPoint à trois composantes (x, y, z):
Regarde ce billet ou j'avais fait une comparaison entre la méthode MEL et la méthode API. Elle renvoi une liste de coordonnée par vertex:
Récupérer rapidement la position des vertices d'un mesh Maya
Avec ça tu doit pouvoir créer des émetteurs à ces positions.
Un autre billet très intéressant qui pourra t'aider un peu (c'est du high level celui là! ^^'):
Remplir un mesh de spheres dans Maya: La méthode d'un sénior!
Bon courage! ;)
Dorian
hey thank you so much for this in depth tutorial :D ..
its very difficult to find MAYA API (python) information.
keep up the good work \m/
Thanks :)
Merveilleux!! Excellent work, and a beautiful explanation! I'm very glad my code was useful to you. In my opinion, ce genre d'informations manquent sur l'internet de l'infographie /en general/.
Bravo! Encore!!
P
No, thanks to you! :)