:longBar:

In advance, sorry for english. :baffed:

Here is what we want to achieve:

projection_mesh_api_015.png

A mesh projected on another.

And the scene from which we start:

projection_mesh_api_001.png

:longBar:
Sommaire
:longBar:
Theory

Once again, we begin with theory.

In facts, you'll see it's pretty simple because the harder part (the intersection) will be handle through an API call:

OpenMaya.MFnMesh.closestIntersection( ... )

This method handle intersection of a ray (point+direction) on a mesh and return some infos (the most important: The position of the projected point).

Basically, we need three things:

  • A point of origin (the one we want to project)
  • A direction (a vector)
  • A destination mesh

projection_mesh_api_002.png

The point of origin will be, of course, each vertex of the mesh to project (here, vertex numbers's are in yellow).

Direction will be the normal of the vertex to project (in green on the image. Okay, we do not see too much but put it a little good will damn! :cayMal: ).

And the destination mesh will obviously be the mesh that will receive the plan (in our case, a sphere).

So we recover, each time, a point and a normal (yellow crosses).

projection_mesh_api_003.png

Forgive my "half-of-a-jpeg-dollar-moldy" diagram :pasClasse:
:longBar:
Start code

Here is the the base 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

If you already had wrote a Maya node in Python, this code shouldn't make you too afraid.

Quick to explain for others :baffed: .

import sys
 
import maya.OpenMaya as OpenMaya
import maya.OpenMayaMPx as OpenMayaMPx

Import the main modules.

  • The sys module is used to create error message when Maya load/unload the plugin (see below).
  • The other two are used to call Maya API methods.

Nothing complicated.

kPluginNodeTypeName = "projectMesh"
 
kPluginNodeId = OpenMaya.MTypeId( 0x80000 )
  • kPluginNodeTypeName is a simple variable called below to give a name to you node type.
  • kPluginNodeId is a value serving as an id for the node when it is written in binary files (mb). 0x80000 to 0xfffff are used by Maya samples. See (documentation|http://download.autodesk.com/us/maya/2011help/API/class_m_type_id.html] for more 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

Here, the class is an instance of the MPxNode object which is himself a class done to create custom nodes (it's a fairly complex part I will not go througth in this tutorial as it deserves a full post :jdicajdirien: ).

The __init__ method is the first executed when the class is created. It simply initialise the MPxNode class.

The compute method is the one we will work the most. It's a herited methode from MPxNode. The part of the code "which do something". :sourit:

If you're not familiar with classes and Python, this part of the code may seem complex, but don't worry, the only important part is the compute method.

# creator
def nodeCreator():
	return OpenMayaMPx.asMPxPtr( projectMeshNode() )

A function, executed when initializing the plugin, which returns a pointer to the created class (and therefore, the node).

Python having no notion of pointer and Maya having need, in particular, to initialize the plugins, Autodesk has developed the OpenMayaMPx.asMPxPtr method (more informations here).

Once again, it's something basic, you put it, you do not think :bete: .

# initializer
def nodeInitializer() :
 
	return

This method is also called during the node creation and allows (among others) to initialize the node attributes. This will be the first one that we will fulfill. For now, it make nothing at all.

# 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

Let me tell you, this is really a "copy-paste" I've done from examples. Basically, these functions are called when loading/unloading plugins. They are used to register/unregister the plugins from Maya sessions.

Their behavior is simple, I invite you to review this code snippet for yourself (it doesn't harm! :gniarkgniark: ).

At this point you should be able to create your python node and load it in Maya

projection_mesh_api_004.png

projection_mesh_api_005.png

projection_mesh_api_006.png

projection_mesh_api_007.png

And by creating it as follow:

createNode projectMesh;
// Result: projectMesh1 //

projection_mesh_api_008.png

To the extent that there is no attribute, this node does absolutely nothing! :sourit:

:longBar:
Attributes preparation

As promised, we will start by the nodeInitializer() method which initialize the node attributes.

We will need three attributes:

  • Two inputs: The mesh projecting his vertex and the mesh which recieve them.
  • An output: The output, projected mesh.

Here we go! :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 )

The first line:

typedAttr = OpenMaya.MFnTypedAttribute()

Creates an "object" (known in the Maya API as "Function Set") that will help us to manipulate the attributes (especially build them):

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

We create attributes. Nothing complicated (see documentation).

Some details on used arguments:

  • The full attribute name (long name).
  • The short attribute name (short name).
  • The attribute "type" (as API type).

The following methods each attribute declaration (setReadable, setWritable, setStorable) add special things to the latest created attribute (see documentation for more precisions).

# Add the attributes to the node
projectMeshNode.addAttribute( projectMeshNode.inputMeshSrc )
projectMeshNode.addAttribute( projectMeshNode.inputMeshTarget )
projectMeshNode.addAttribute( projectMeshNode.outputMesh )

As the comment said, this part add/connect attributes created above to the node.

# Set the attribute dependencies
projectMeshNode.attributeAffects( projectMeshNode.inputMeshSrc, projectMeshNode.outputMesh )
projectMeshNode.attributeAffects( projectMeshNode.inputMeshTarget, projectMeshNode.outputMesh )

This part is very important! :papi:

It allows you to define "dependencies" between attributes.

In our case:

  • If the inputMeshSrc attribute change, outputMesh attribute also change.
  • If the inputMeshTarget attribute change, outputMesh attribute also change.

If these lines are not set, the compute method of the node we are creating will never be launched. The node will never be updated.

You can download the python node in the current state here:

>> projectMesh001.7z <<

:longBar:
Prepare the scene

Before we actually code the behavior of the node, we need to connect some geometry to our future node.

Create a scene that looks like this:

projection_mesh_api_001.png

Also create a third mesh, which will return the geometry of our future node (which will be the projected mesh).

In my case: A pSphere. But it can be anything. As it is a mesh node.

You can even create the mesh node by hand.

Place it to the center of the scene (0,0,0) so there have no offset between the projected mesh and his target.

projection_mesh_api_009.png

To avoid having to create the connections each time, here are two small MEL codes that help you to test your node (your nodes must be well named).

To load all:

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;

And unload all:

delete projectMesh1;
flushUndo;
unloadPlugin( "projectMesh" );

And that's it! Now take a deep breath, we jump!

:longBar:
The compute method

The first thing to test is the presence of a connection on your outputMesh attribute. Actually, if your node is connected to anything, it should not be calculated:

def compute( self, plug, data ) :
 
	if plug == self.outputMesh:
 
		print "compute"
 
	else:
		return OpenMaya.MStatus.kUnknownParameter
 
	return OpenMaya.MStatus.kSuccess

Once we are sure connections are made, we get input attributes:

if plug == self.outputMesh:
 
	# get the inputMeshTarget (return MDataHandle)
	inMeshSrcHandle = data.inputValue( self.inputMeshSrc )
	inMeshTargetHandle = data.inputValue( self.inputMeshTarget )

This is a "connection" to the block of data attributes. This is the first step to get the value (here it's a kMesh so it will be a little different) of an attribute.

Python being an untyped language (both its main strength but also its main flaw ...), I tend to write in comments the Maya API type of data that I get.

Otherwise, it's quick to not knowing at all what type is what variable (types in the Maya API is no lack :aupoil: ).

However, everyone has their own method! If you have a over-developed cortex, if you want to play the "I dont care types are for noobs", if a code that you are the only person who can read it give you the feeling to be a strong male and if you want to justify your BAC +5 (in this day and age it's certainly not your salary that should do it). Do not hesitate, code as TD pork: No comments, one letter variables and whatnot like that... Your colleagues will return the favor. :sourit:

But if you're more modest and would quickly learn the Maya API, I urge you to write Maya API types directly in comments of your code. Besides being clearer, it still requires to known/search, when writing variables, what type it is.

After that, we check our two connected attributes are 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

And we get them as such:

#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

I invite you to look at the MDataHandle documentation just to see what we can get from an attribute.

As mentioned in the comment, we get a MObject. This object type is the "all-purpose-type" of Maya.

This MObject is just a transitionnal object. Actually, it is rarely used directly.

In Maya, to modify/malipulate objects, we often use "Function Set". They follow the pattern: MFn*Type*.

Here, to manipulate meshs, we will get a function set of mesh: MFnMesh.

# get the MFnMesh of the twice attr
mFnMeshSrc = OpenMaya.MFnMesh( meshSrc )
mFnMeshTarget = OpenMaya.MFnMesh( meshTarget )

What interests us now is to have a list of all vertices of the "source mesh" (one which will be projected to the "target mesh") to create another array that will contain changed vertex positions:

outMeshMPointArray = OpenMaya.MPointArray()	# create an array of vertex wich will contain the outputMesh vertex
mFnMeshSrc.getPoints(outMeshMPointArray)	# get the point in the space

The first line creates an array of type MPointArray.

The second line fills it with the values ​​of the "source mesh" vertex.

How to write is a bit confusing ("upside down" will say some :reflechi: ) but that's the way getPoint works, as many other functions of the Maya API.

Rather than return the result, it is stored in the variable given as argument.

We now have a MPointArray filled with his vertex positions.

The idea now is to change vertex positions to make them matching the projected position on the "target mesh".

But here... The vertex positions that you get in your MPointArray are in "object space". That mean, relative to the center of the object.

Nous allons nous heurter à un vrai problème. The big one! The ultimate: The matrices! *Voix qui résonne* :enerve: We will face a real problem. The big one! The ultimate: The Matrix! *Voice echoing* :enerve:

:longBar:
Matrices explained to CG artists

When you are CG artists, we hear about it without really knowing what it is :bete: .

Add to that what we find on the net is very academic and "too mathematical" (It shows how to multiply a matrix without explaining why it is necessary to do so).

All this so that we don't necessarily see the connection with our work.

I'll modestly try to explain this from a "CG artist" point of view :mayaProf: .

photoMatrix.jpg

Photography by My Melting Brain under Creative Common by-nc-sa. Thanks to him! :sourit:

Create a cube.

projection_mesh_api_010.png

You may have noticed, once your cube is created, that it has a "pivot point" with informations (position, rotation, scale, etc. ...). Well this point can be a "mathematical link" between the vertex points of your cube and "the world" (center coordinates of the "world" are 0,0,0).

projection_mesh_api_011.png

If this "transform" node didn't exist, your object would be at center of the scene. And to move it, it should move the all vertex positions of the cube.

The transform node acts like a "parent" of the vertex of your cube. In this way, the vertex positions of the cube doesn't move. For example, a vertex of a cube placed at 1,1,1 (relative to the pivot of the cube so) will remain at 1,1,1 regardless of the position of the transform.

projection_mesh_api_012.png

Here, we move the pivot, not the cube vertices's that they don't change position relative to the pivot, the pivot that changes position relative to the world.

But some operations (in our case, get if the vertex is face to a different mesh) requires that the coordinates of all the entities are a common reference, the reference world.

Comic demonstration:

projection_mesh_api_bd_001.png

- Hi! I am vertex[0] and I'm on (1,1,1).
- Hi! I am vertex[0] and I'm on (1,1,1).
Two vertices at different coordinates in "world space"...

projection_mesh_api_bd_002.png

...but at the same place relative to their respective centers.

projection_mesh_api_bd_003.png

- (insult)
- (insult)
Not easy to make calculations.

projection_mesh_api_bd_004.png

You see the prob?

Whereas if we chose, as common reference point, the center of the world.

projection_mesh_api_bd_005.png

- Hi! I am vertex[0] and I'm on (4,2,3) relative to world.
- Hi! I am vertex[0] and I'm on (3,1,2) relative to world.

It's much easier.

projection_mesh_api_bd_006.png

projection_mesh_api_bd_007.png

- Friend!
- Friend!

projection_mesh_api_bd_008.png

projection_mesh_api_bd_009.png

- Nevertheless, I am still higher than you!
- Whait... What? o_O

Okay, now we know the vertex coordinates in their "object space", we must know how get them in "world space".

The basic principle that immediately comes to mind is: We add vertex positions (in object space) to its pivot point (him, in world space).

Example:

If pVertex the position of a vertex and cubePosition the position, in world space, of the pivot point of the cube:

cubePositionX + pVertexInCubeX = pVertexInWorldX

But surely you can imagine, it's more complicated... :siffle:

Indeed, in the case of rotations and scale, it is not enough of a few additions to solve the problem.

Note: Whether a translation, a rotation, or scaling a mesh, all comes down to vertex moves in the world.

The truth is that all this "parameters" can be put into a single "object" that we call a matrix. This matrix will help us to make calculations (as Maya saves us but if you're interested, here is an example of calculates a rotation matrix) to get vertex positions in world space.

As I said, Maya gives us easely access to this object through inclusiveMatrix (There are several matrix types, we will focus on this one).

So at this stage, we have two things:

  • Vertex positions relative to the object (in "object space").
  • A matrix of the object (which is world relative, "world space").

We need to "convert" vertex positions from "space object" to "world space". A "change of reference" (sorry guys, don't know the english word for that :pasClasse: ). So you get an absolute position ("world space").

And to get vertex position in the "world space", "simply" multiply vertex position matrix {x, y, z} by inclusion matrix of the object. (I wrote "simply" in quotes because multiplying a matrix is not as simple as 2x2... :redface: )

Note: I would not do demonstration on "how to calculate a matrix", internet is full of examples and explanations.

And here's the formula:

positionGlobale = positionLocale * inclusiveMatrix

It's almost like the Pythagorean theorem. Who cares how it works, until you know when to use it! :baffed:

Voila! I hope this little explanation you will shed some light on why the matrices are. :sourit:

:longBar:
Back to the code!

Get inclusive matrix is 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

The first line retrieves the dagPath of the source mesh. We can consider the dagPath as the equivalent of the transform node of an object in Maya. Where all the informations about transformations (positions, rotations, scales, etc ...) are stored.

The second line retrieves the inclusiveMatrix of the source mesh as a MMatrix type.

Now we can go through each vertex:

:longBar:
Go through each vertex and modify it

The beginning of the main loop looks like this:

for i in range( outMeshMPointArray.length() ) :
 
	inMeshMPointTmp = outMeshMPointArray[i] * inMeshSrcInclusiveMMatrix	# the MPoint of the meshSrc in the worldspace

The loop is quite simple: "i" is incremented by 1 each "lap" to browse the vertex array (MPointArray).

The first thing we do is point multiplication (outMeshMPointArray[i]) by the matrix (inMeshSrcInclusiveMMatrix) to obtain a vertex (inMesgPointTmp) in world space ( with coordinates relatives to the world, 0,0,0).

Now we've (finally) the vertex positioned relative to the world, we will "intersect" it with the target mesh.

As chrysl666 told in his comment, Maya give a simple way to get informations without have to extract matrix and multiply it yourself. Just call your points (and others stuffs) with:

getPoints(OpenMaya.MSpace.kWorld).

This will return points position directly in world space. Thanks to him for this information. :bravo:

intersection.jpg

Photography by MyNameMattersNot under Creative Common by-sa. Thanks to him!

Well, go look at arguments of the OpenMaya.MFnMesh.closestIntersection() method we seen above. Bon, allez regarder les arguments de la méthode OpenMaya.MFnMesh.closestIntersection() que nous avont vu plus haut.

You see there are quite a few. :sourit:

Don't worry, we can ignore many of them. What interests us is the original vertex, his direction (in our case: normal) and the collision point (the hitPoints).

But more subtle, look at the type of the first argument (raySource) expected by the method.

It's a MFloatPoint!

But how convert inMeshMPointTmp (a MPoint) to a MFloatPoint? It's pretty easy in C++, you have to use doubles. After much research, I found a solution. I give it to you:

raySource = OpenMaya.MFloatPoint( inMeshMPointTmp.x, inMeshMPointTmp.y, inMeshMPointTmp.z )

It's not as complicated but if you don't know...

So we have our raySource.

Now the normal:

rayDirection = OpenMaya.MVector()
mFnMeshSrc.getVertexNormal( i, False, rayDirection)
rayDirection *= inMeshSrcInclusiveMMatrix
rayDirection = OpenMaya.MFloatVector(rayDirection.x, rayDirection.y, rayDirection.z)

At this point, you should understand:

  • We create the MVector.
  • We store the current vertex (i) normal of the source mesh in it.
  • We multiply by the matrix ot get this vector in space world.
  • We "convert" it to MFloatVector.

The hitPoint is a simple MFloatPoint:

hitPoint = OpenMaya.MFloatPoint()

And the remaining arguments are:

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

Great thanks to Peter J. Richardson! Sans son billet, I would never have succeeded to code this stuff. This is why we need to "share what you know" on the internet! ;)

Once called closestIntersection, you get a hitPoint in MFloatPoint that you convert into MPoint:

if hit :
	inMeshMPointTmp = OpenMaya.MPoint( hitPoint.x, hitPoint.y, hitPoint.z)

We replaces the current point by our new one:

outMeshMPointArray.set( inMeshMPointTmp, i )

And this is the end of the loop! :D

At this point you've got a MPointArray outMeshMPointArray with values of projected vertices on the target mesh.

So now we need to rebuild the mesh.

:longBar:
Build a mesh

I will not go througt details on "how to create and arrange the variables in the case of the creation of a mesh".

Basically we need:

  • The number of vertices.
  • Le number of ploygons (polygons + triangles if there is).
  • A array of points (All vertices one behind others with they XYZ coordinates).
  • An array listing the number of vertices by polygons (Example: 4,4,4,4,3,4,3,4,3,4,4,etc...).
  • An array of vertex indices (Example: 1,2,3,4,3,4,5,6,5,6,7,8, etc...).

You will have understood, we already have the points array (the third point). Otherwise, we silly gets values from the original mesh.

Let's start! :hehe:

At first, we must create a MFnMeshData function set.

# create a mesh that we will feed!
newDataCreator = OpenMaya.MFnMeshData()

This function set will allow us to create a MObject that we can "fill" with data of the future mesh.

newOutputData = newDataCreator.create()	# Return MObject

As I said above: We need to get all informations (except the vertex array) of the source mesh:

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() filled two arrays with informations needed to create the mesh. See above.

Once we have that, we create the mesh:

meshFS = OpenMaya.MFnMesh()
meshFS.create(outMeshNumVtx, outMeshNumPolygons, outMeshMPointArray, outMeshPolygonCountArray, outMeshVtxArray, newOutputData)

The principle is quite simple:

  • We create a MFnMesh function set.
  • We create the mesh giving all arguments re le mesh en donnant tout les arguments (collected above) via meshFS.create().

Once the mesh is created, we get the MDataHandle from the outputMesh connection to put the MObject we just fill in it: The mesh!

# Store them on the output plugs
outputMeshHandle = data.outputValue( self.outputMesh )
outputMeshHandle.setMObject( newOutputData )

Once this is done, we say, via MDataBlock.setClean(), to the dependency graph that the connection has been updated.

# tell to the dependency graph the connection is clean
data.setClean( plug )

It's over! :youplaBoum:

If you well followed the tutorial (and if I did not make errors ( :baffed: ), you should have a node that works correctly (place the plan so that it "target" the sphere):

projection_mesh_api_013.png

Of course, if vertices aren't projected on the sphere, it return to their original positions:

projection_mesh_api_014.png

:longBar:
Source code and conclusion

At this point, you should have understood the principle of matrices (If this is not the case, don't hesitate to go deeply, you will see 3D in a different way! :sourit: ), be able to recover components of a mesh and create a mesh from scratch.

There is the source code: projectMesh.7z

And voila! I just finished another big tutorial. I hope it has been informative and that you can begin to do interesting things with Maya API. :banaeyouhou:

Such informations is lacking on the Internet on CG web site.

I encourage all seniors who would pass by and who would learn something interesting reading this post to do not hesitate to "give back": If you are competent in a field, share! I did. :hihi:

If I'm wrong somewhere, if a point does not seem clear or if there is an error, please tell me. :pasClasse:

See you soon!

:marioCours:

Note: As you could read, my english is far from being perfect. Don't hesitate to leave a comment if you found a better way to explain some stuff.

:longBar:
Update: Do this without Python

Well, it's not really the goal but since Kel Solar talk about in comments I also give you a way to do this using the built in Maya. :seSentCon:

Select the target mesh:

projection_mesh_api_016.png

Select the source mesh:

projection_mesh_api_017.png

Open transfert attributes options:

projection_mesh_api_018.png

Set options as follow:

projection_mesh_api_019.png

Basically, we only transfer the vertex positions in world space by projecting them along their normal.

projection_mesh_api_020.png

And that's it!

You can rotate the source mesh and it's updated! :laClasse:

Voila! That way, people who came to find a quick solution will not be frustrated! :sourit: