:longBar:

Voici ce que nous souhaitons obtenir:

projection_mesh_api_015.png

Un mesh projeté sur un autre.

Et la scène à partir de laquelle nous partons:

projection_mesh_api_001.png

:longBar:
Sommaire
:longBar:
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.

projection_mesh_api_002.png

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).

projection_mesh_api_003.png

Pardonnez mon shéma à dix-francs-six-sous-convertion-jpeg-moisie :pasClasse:
:longBar:
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:

projection_mesh_api_004.png

projection_mesh_api_005.png

projection_mesh_api_006.png

projection_mesh_api_007.png

Et en le créant comme suis:

createNode projectMesh;
// Result: projectMesh1 //

projection_mesh_api_008.png

Dans la mesure ou il n'y a aucun attribut, ce node ne fait absolument rien! :sourit:

:longBar:
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:

>> projectMesh001.7z <<

:longBar:
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:

projection_mesh_api_001.png

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.

projection_mesh_api_009.png

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!

:longBar:
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 TD porc: 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:

:longBar:
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: .

photoMatrix.jpg

Photo par My Melting Brain sous licence Créative by-nc-sa. Merci à lui! :sourit:

Créez un cube.

projection_mesh_api_010.png

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).

projection_mesh_api_011.png

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.

projection_mesh_api_012.png

Ici, on déplace le pivot, pas les vertex du cube qui eux, ne change pas de place par rapport au pivot, c'est le pivot qui change de place par rapport au monde.

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é:

projection_mesh_api_bd_001.png

Deux vertex placés à différents endroits dans l'espace monde...

projection_mesh_api_bd_002.png

...mais au même endroit par rapport à leurs centres respectifs.

projection_mesh_api_bd_003.png

Pas pratique pour faire des calcules.

projection_mesh_api_bd_004.png

Alors que si on choisi, comme point de repère commun, le centre du monde.

projection_mesh_api_bd_005.png

C'est beaucoup plus facile.

projection_mesh_api_bd_006.png

projection_mesh_api_bd_007.png

projection_mesh_api_bd_008.png

projection_mesh_api_bd_009.png

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:

:longBar:
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.
:longBar:
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.

intersection.jpg

Photo par MyNameMattersNot sous licence Créative by-sa. Merci à lui!

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.

:longBar:
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):

projection_mesh_api_013.png

Bien sur, si les vertex ne sont pas projeté sur la sphère, il retourne à leur position d'origine:

projection_mesh_api_014.png

:longBar:
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!

:marioCours:
:longBar:
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:

projection_mesh_api_016.png

Sélectionner le mesh source:

projection_mesh_api_017.png

Ouvrez les options du transfert d'attributs:

projection_mesh_api_018.png

Settez les options comme suis:

projection_mesh_api_019.png

En gros, on ne transfert que la position des vertex dans l'espace monde en les projetant le long de leur normale.

projection_mesh_api_020.png

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: