ファイル名: boundingBoxDeformer.py
*サンプル出力: *次の出力では、変形した球に緑の Phong マテリアルを割り当てています。「メッシュの膨張」アトリビュートの値を大きくすると、メッシュはバウンディング ボックスをさらに塗り潰そうとします。
*プログラムの概要: *以下のプラグイン コードは、メッシュを「膨張」させてバウンディング ボックスの境界内に拡張させる動作を持つ新しいデフォーマを定義します。メッシュがボックスの境界にヒットするまで拡張できる空間を持たせるために、このバウンディング ボックスのサイズを「バウンディング ボックス スケール」アトリビュートによってスケールすることができます。メッシュの膨張は、「メッシュの膨張」アトリビュートによってコントロールされます。プラグイン クラスは MPxDeformerNode
を継承し、MPxDeformer.deform()
関数をオーバーライドしてその動作を定義します。Python でデフォーマ ノードを作成するときに次のポイントを覚えておくと便利です。
ビルトイン デフォーマ ノード アトリビュート: クラスは MPxDeformerNode
を継承することにより、入力および出力メッシュ アトリビュートにアクセスします。これらのアトリビュートは、OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom
および OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom
を介してそれぞれ利用可能です。これらの変数は、C++ ヘッダ ファイルの Python への変換時に SWIG ツールによって生成されます。同じ処理によって、デフォーマはビルトイン「エンベロープ」アトリビュートにアクセスします。このアトリビュートは、OpenMayaMPx.cvar.MPxGeometryFilter_envelope
を介して利用可能です。このエンベロープ アトリビュートを使用して、デフォーマがメッシュに与える影響の量を指定します。
注: これは、Maya 2016 の新機能です。Maya 2016 よりも前のバージョンの Maya では、入力メッシュと出力メッシュのアトリビュートはそれぞれ
OpenMayaMPx.cvar.MPxDeformerNode_inputGeom
およびOpenMayaMPx.cvar.MPxDeformerNode_outputGeom
を介して利用でき、エンベロープはOpenMayaMPx.cvar.MPxDeformerNode_envelope
を介して利用できます。
deform() の入力ジオメトリの取得: deform()
関数に渡されるジオメトリ イテレータ(MItGeometry
)は、メッシュの各頂点の位置だけを提供します。各頂点の法線を取得するには、実際のメッシュ オブジェクトへのリファレンスが必要になります。入力メッシュ オブジェクトを返す getDeformerInputGeometry()
というヘルパー関数が定義されています。その後、返されたメッシュに対して MFnMesh
関数セットを使用して、頂点法線のリストを取得できます(MFnMesh.getVertexNormals()
による)。
Python API 2.0: このサンプルは、MPxDeformerNode
がこの API でまだ公開されていないため、使用できません。
Python API 1.0 (Maya 2016 よりも前のバージョン用):
# boundingBoxDeformer.py
import sys
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya
# Plug-in information:
kPluginNodeName = 'boundingBoxDeformer' # The name of the node.
kPluginNodeId = OpenMaya.MTypeId( 0xBEEF8 ) # A unique ID associated to this node type.
# This determines how fast the vertices of the mesh will "expand" towards the boundary of the bounding box.
vertexIncrement = 0.1
##########################################################
# Plug-in
##########################################################
class MyDeformerNode(OpenMayaMPx.MPxDeformerNode):
# Static variable(s) which will later be replaced by the node's attribute(s).
boundingBoxScaleAttribute = OpenMaya.MObject()
meshInflationAttribute = OpenMaya.MObject()
def __init__(self):
''' Constructor. '''
# (!) Make sure you call the base class's constructor.
OpenMayaMPx.MPxDeformerNode.__init__(self)
def deform(self, pDataBlock, pGeometryIterator, pLocalToWorldMatrix, pGeometryIndex):
''' Deform each vertex using the geometry iterator. '''
# The envelope determines the overall weight of the deformer on the mesh.
# The envelope is obtained via the OpenMayaMPx.cvar.MPxDeformerNode_envelope variable.
# This variable and others like it are generated by SWIG to expose variables or constants declared in C++ header files.
envelopeAttribute = OpenMayaMPx.cvar.MPxDeformerNode_envelope
envelopeValue = pDataBlock.inputValue( envelopeAttribute ).asFloat()
# Get the value of the mesh inflation node attribute.
meshInflationHandle = pDataBlock.inputValue( MyDeformerNode.meshInflationAttribute )
meshInflation = meshInflationHandle.asDouble()
# Get the value of the bounding box scale node attribute.
boundingBoxScaleHandle = pDataBlock.inputValue( MyDeformerNode.boundingBoxScaleAttribute )
boundingBoxScale = boundingBoxScaleHandle.asDouble()
# Get the input mesh from the datablock using our getDeformerInputGeometry() helper function.
inputGeometryObject = self.getDeformerInputGeometry(pDataBlock, pGeometryIndex)
# Compute the bounding box using the input the mesh.
boundingBox = self.getBoundingBox( inputGeometryObject, boundingBoxScale )
# Obtain the list of normals for each vertex in the mesh.
normals = OpenMaya.MFloatVectorArray()
meshFn = OpenMaya.MFnMesh( inputGeometryObject )
meshFn.getVertexNormals( True, normals, OpenMaya.MSpace.kTransform )
# Iterate over the vertices to move them.
global vertexIncrement
while not pGeometryIterator.isDone():
# Obtain the vertex normal of the geometry. This normal is the vertex's averaged normal value if that
# vertex is shared among several polygons.
vertexIndex = pGeometryIterator.index()
normal = OpenMaya.MVector( normals[vertexIndex] ) # Cast the MFloatVector into a simple vector.
# Increment the point along the vertex normal.
point = pGeometryIterator.position()
newPoint = point + ( normal * vertexIncrement * meshInflation * envelopeValue )
# Clamp the new point within the bounding box.
self.clampPointInBoundingBox( newPoint, boundingBox )
# Set the position of the current vertex to the new point.
pGeometryIterator.setPosition( newPoint )
# Jump to the next vertex.
pGeometryIterator.next()
def getDeformerInputGeometry(self, pDataBlock, pGeometryIndex):
'''
Obtain a reference to the input mesh. This mesh will be used to compute our bounding box, and we will also require its normals.
We use MDataBlock.outputArrayValue() to avoid having to recompute the mesh and propagate this recomputation throughout the
Dependency Graph.
OpenMayaMPx.cvar.MPxDeformerNode_input and OpenMayaMPx.cvar.MPxDeformerNode_inputGeom are SWIG-generated
variables which respectively contain references to the deformer's 'input' attribute and 'inputGeom' attribute.
'''
inputAttribute = OpenMayaMPx.cvar.MPxDeformerNode_input
inputGeometryAttribute = OpenMayaMPx.cvar.MPxDeformerNode_inputGeom
inputHandle = pDataBlock.outputArrayValue( inputAttribute )
inputHandle.jumpToElement( pGeometryIndex )
inputGeometryObject = inputHandle.outputValue().child( inputGeometryAttribute ).asMesh()
return inputGeometryObject
def getBoundingBox(self, pMeshObj, pBoundingBoxScale):
''' Calculate a bounding box around the mesh's vertices. '''
# Create the bounding box object we will populate with the points of the mesh.
boundingBox = OpenMaya.MBoundingBox()
meshFn = OpenMaya.MFnMesh( pMeshObj )
pointArray = OpenMaya.MPointArray()
# Get the points of the mesh in its local coordinate space.
meshFn.getPoints( pointArray, OpenMaya.MSpace.kTransform )
for i in range( 0, pointArray.length() ):
point = pointArray[i]
boundingBox.expand( point )
# Expand the bounding box according to the scaling factor.
newMinPoint = boundingBox.min() * pBoundingBoxScale
newMaxPoint = boundingBox.max() * pBoundingBoxScale
boundingBox.expand( newMinPoint )
boundingBox.expand( newMaxPoint )
return boundingBox
def clampPointInBoundingBox(self, pPoint, pBoundingBox):
''' Ensure that the given point is contained within the bounding box's min/max coordinates. '''
# Define a quick clamping function for internal use within this method body.
def clamp(pValue, pMin, pMax):
return max( pMin, min( pValue, pMax ) )
pPoint.x = clamp( pPoint.x, pBoundingBox.min().x, pBoundingBox.max().x )
pPoint.y = clamp( pPoint.y, pBoundingBox.min().y, pBoundingBox.max().y )
pPoint.z = clamp( pPoint.z, pBoundingBox.min().z, pBoundingBox.max().z )
##########################################################
# Plug-in initialization.
##########################################################
def nodeCreator():
''' Creates an instance of our node class and delivers it to Maya as a pointer. '''
return OpenMayaMPx.asMPxPtr( MyDeformerNode() )
def nodeInitializer():
''' Defines the input and output attributes as static variables in our plug-in class. '''
# The following MFnNumericAttribute function set will allow us to create our attributes.
numericAttributeFn = OpenMaya.MFnNumericAttribute()
#==================================
# INPUT NODE ATTRIBUTE(S)
#==================================
# Define the scaling factor attribute of the bounding box around the mesh.
MyDeformerNode.boundingBoxScaleAttribute = numericAttributeFn.create( 'boundingBoxScale', 'bs', OpenMaya.MFnNumericData.kDouble, 1.5 )
numericAttributeFn.setMin( 1.0 )
numericAttributeFn.setMax( 3.0 )
numericAttributeFn.setStorable( True )
numericAttributeFn.setWritable( True )
numericAttributeFn.setReadable( False )
MyDeformerNode.addAttribute( MyDeformerNode.boundingBoxScaleAttribute )
# Define a mesh inflation attribute, responsible for actually moving the vertices in the direction of their normals.
MyDeformerNode.meshInflationAttribute = numericAttributeFn.create( 'meshInflation', 'mi', OpenMaya.MFnNumericData.kDouble, 10.0 )
numericAttributeFn.setMin( 1.0 )
numericAttributeFn.setMax( 50.0 )
numericAttributeFn.setStorable( True )
numericAttributeFn.setWritable( True )
numericAttributeFn.setReadable( False )
MyDeformerNode.addAttribute( MyDeformerNode.meshInflationAttribute )
''' The input geometry node attribute is already declared in OpenMayaMPx.cvar.MPxDeformerNode_inputGeom '''
#==================================
# OUTPUT NODE ATTRIBUTE(S)
#==================================
''' The output geometry node attribute is already declared in OpenMayaMPx.cvar.MPxDeformerNode_outputGeom '''
#==================================
# NODE ATTRIBUTE DEPENDENCIES
#==================================
# If any of the inputs change, the output mesh will be recomputed.
MyDeformerNode.attributeAffects( MyDeformerNode.boundingBoxScaleAttribute, OpenMayaMPx.cvar.MPxDeformerNode_outputGeom )
MyDeformerNode.attributeAffects( MyDeformerNode.meshInflationAttribute, OpenMayaMPx.cvar.MPxDeformerNode_outputGeom )
def initializePlugin( mobject ):
''' Initialize the plug-in '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.registerNode( kPluginNodeName, kPluginNodeId, nodeCreator,
nodeInitializer, OpenMayaMPx.MPxNode.kDeformerNode )
except:
sys.stderr.write( 'Failed to register node: ' + kPluginNodeName )
raise
def uninitializePlugin( mobject ):
''' Uninitializes the plug-in '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.deregisterNode( kPluginNodeId )
except:
sys.stderr.write( 'Failed to deregister node: ' + kPluginNodeName )
raise
##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:
import maya.cmds as cmds
cmds.loadPlugin( 'boundingBoxDeformer.py' )
cmds.polySphere()
cmds.deformer( type='boundingBoxDeformer' )
'''
Python API 1.0 (Maya 2016 で有効):
# boundingBoxDeformer.py
import sys
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya
# Plug-in information:
kPluginNodeName = 'boundingBoxDeformer' # The name of the node.
kPluginNodeId = OpenMaya.MTypeId( 0xBEEF8 ) # A unique ID associated to this node type.
# This determines how fast the vertices of the mesh will "expand" towards the boundary of the bounding box.
vertexIncrement = 0.1
# Some global variables were moved from MPxDeformerNode to MPxGeometryFilter.
# Set some constants to the proper C++ cvars based on the API version.
import maya.cmds as cmds
kApiVersion = cmds.about(apiVersion=True)
if kApiVersion < 201600:
kInput = OpenMayaMPx.cvar.MPxDeformerNode_input
kInputGeom = OpenMayaMPx.cvar.MPxDeformerNode_inputGeom
kOutputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom
kEnvelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope
else:
kInput = OpenMayaMPx.cvar.MPxGeometryFilter_input
kInputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom
kOutputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom
kEnvelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope
##########################################################
# Plug-in
##########################################################
class MyDeformerNode(OpenMayaMPx.MPxDeformerNode):
# Static variable(s) which will later be replaced by the node's attribute(s).
boundingBoxScaleAttribute = OpenMaya.MObject()
meshInflationAttribute = OpenMaya.MObject()
def __init__(self):
''' Constructor. '''
# (!) Make sure you call the base class's constructor.
OpenMayaMPx.MPxDeformerNode.__init__(self)
def deform(self, pDataBlock, pGeometryIterator, pLocalToWorldMatrix, pGeometryIndex):
''' Deform each vertex using the geometry iterator. '''
# The envelope determines the overall weight of the deformer on the mesh.
# The envelope is obtained via the OpenMayaMPx.cvar.MPxDeformerNode_envelope (pre Maya 2016) or
# OpenMayaMPx.cvar.MPxGeometryFilter_envelope (Maya 2016) variable.
# This variable and others like it are generated by SWIG to expose variables or constants declared in C++ header files.
envelopeAttribute = kEnvelope
envelopeValue = pDataBlock.inputValue( envelopeAttribute ).asFloat()
# Get the value of the mesh inflation node attribute.
meshInflationHandle = pDataBlock.inputValue( MyDeformerNode.meshInflationAttribute )
meshInflation = meshInflationHandle.asDouble()
# Get the value of the bounding box scale node attribute.
boundingBoxScaleHandle = pDataBlock.inputValue( MyDeformerNode.boundingBoxScaleAttribute )
boundingBoxScale = boundingBoxScaleHandle.asDouble()
# Get the input mesh from the datablock using our getDeformerInputGeometry() helper function.
inputGeometryObject = self.getDeformerInputGeometry(pDataBlock, pGeometryIndex)
# Compute the bounding box using the input the mesh.
boundingBox = self.getBoundingBox( inputGeometryObject, boundingBoxScale )
# Obtain the list of normals for each vertex in the mesh.
normals = OpenMaya.MFloatVectorArray()
meshFn = OpenMaya.MFnMesh( inputGeometryObject )
meshFn.getVertexNormals( True, normals, OpenMaya.MSpace.kTransform )
# Iterate over the vertices to move them.
global vertexIncrement
while not pGeometryIterator.isDone():
# Obtain the vertex normal of the geometry. This normal is the vertex's averaged normal value if that
# vertex is shared among several polygons.
vertexIndex = pGeometryIterator.index()
normal = OpenMaya.MVector( normals[vertexIndex] ) # Cast the MFloatVector into a simple vector.
# Increment the point along the vertex normal.
point = pGeometryIterator.position()
newPoint = point + ( normal * vertexIncrement * meshInflation * envelopeValue )
# Clamp the new point within the bounding box.
self.clampPointInBoundingBox( newPoint, boundingBox )
# Set the position of the current vertex to the new point.
pGeometryIterator.setPosition( newPoint )
# Jump to the next vertex.
pGeometryIterator.next()
def getDeformerInputGeometry(self, pDataBlock, pGeometryIndex):
'''
Obtain a reference to the input mesh. This mesh will be used to compute our bounding box, and we will also require its normals.
We use MDataBlock.outputArrayValue() to avoid having to recompute the mesh and propagate this recomputation throughout the
Dependency Graph.
OpenMayaMPx.cvar.MPxDeformerNode_input and OpenMayaMPx.cvar.MPxDeformerNode_inputGeom (for pre Maya 2016) and
OpenMayaMPx.cvar.MPxGeometryFilter_input and OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom (Maya 2016) are SWIG-generated
variables which respectively contain references to the deformer's 'input' attribute and 'inputGeom' attribute.
'''
inputAttribute = OpenMayaMPx.cvar.MPxGeometryFilter_input
inputGeometryAttribute = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom
inputHandle = pDataBlock.outputArrayValue( inputAttribute )
inputHandle.jumpToElement( pGeometryIndex )
inputGeometryObject = inputHandle.outputValue().child( inputGeometryAttribute ).asMesh()
return inputGeometryObject
def getBoundingBox(self, pMeshObj, pBoundingBoxScale):
''' Calculate a bounding box around the mesh's vertices. '''
# Create the bounding box object we will populate with the points of the mesh.
boundingBox = OpenMaya.MBoundingBox()
meshFn = OpenMaya.MFnMesh( pMeshObj )
pointArray = OpenMaya.MPointArray()
# Get the points of the mesh in its local coordinate space.
meshFn.getPoints( pointArray, OpenMaya.MSpace.kTransform )
for i in range( 0, pointArray.length() ):
point = pointArray[i]
boundingBox.expand( point )
# Expand the bounding box according to the scaling factor.
newMinPoint = boundingBox.min() * pBoundingBoxScale
newMaxPoint = boundingBox.max() * pBoundingBoxScale
boundingBox.expand( newMinPoint )
boundingBox.expand( newMaxPoint )
return boundingBox
def clampPointInBoundingBox(self, pPoint, pBoundingBox):
''' Ensure that the given point is contained within the bounding box's min/max coordinates. '''
# Define a quick clamping function for internal use within this method body.
def clamp(pValue, pMin, pMax):
return max( pMin, min( pValue, pMax ) )
pPoint.x = clamp( pPoint.x, pBoundingBox.min().x, pBoundingBox.max().x )
pPoint.y = clamp( pPoint.y, pBoundingBox.min().y, pBoundingBox.max().y )
pPoint.z = clamp( pPoint.z, pBoundingBox.min().z, pBoundingBox.max().z )
##########################################################
# Plug-in initialization.
##########################################################
def nodeCreator():
''' Creates an instance of our node class and delivers it to Maya as a pointer. '''
return OpenMayaMPx.asMPxPtr( MyDeformerNode() )
def nodeInitializer():
''' Defines the input and output attributes as static variables in our plug-in class. '''
# The following MFnNumericAttribute function set will allow us to create our attributes.
numericAttributeFn = OpenMaya.MFnNumericAttribute()
#==================================
# INPUT NODE ATTRIBUTE(S)
#==================================
# Define the scaling factor attribute of the bounding box around the mesh.
MyDeformerNode.boundingBoxScaleAttribute = numericAttributeFn.create( 'boundingBoxScale', 'bs', OpenMaya.MFnNumericData.kDouble, 1.5 )
numericAttributeFn.setMin( 1.0 )
numericAttributeFn.setMax( 3.0 )
numericAttributeFn.setStorable( True )
numericAttributeFn.setWritable( True )
numericAttributeFn.setReadable( False )
MyDeformerNode.addAttribute( MyDeformerNode.boundingBoxScaleAttribute )
# Define a mesh inflation attribute, responsible for actually moving the vertices in the direction of their normals.
MyDeformerNode.meshInflationAttribute = numericAttributeFn.create( 'meshInflation', 'mi', OpenMaya.MFnNumericData.kDouble, 10.0 )
numericAttributeFn.setMin( 1.0 )
numericAttributeFn.setMax( 50.0 )
numericAttributeFn.setStorable( True )
numericAttributeFn.setWritable( True )
numericAttributeFn.setReadable( False )
MyDeformerNode.addAttribute( MyDeformerNode.meshInflationAttribute )
''' The input geometry node attribute is already declared in OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom '''
#==================================
# OUTPUT NODE ATTRIBUTE(S)
#==================================
''' The output geometry node attribute is already declared in OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom '''
#==================================
# NODE ATTRIBUTE DEPENDENCIES
#==================================
# If any of the inputs change, the output mesh will be recomputed.
print dir(OpenMayaMPx.cvar)
MyDeformerNode.attributeAffects( MyDeformerNode.boundingBoxScaleAttribute, kOutputGeom )
MyDeformerNode.attributeAffects( MyDeformerNode.meshInflationAttribute, kOutputGeom )
def initializePlugin( mobject ):
''' Initialize the plug-in '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.registerNode( kPluginNodeName, kPluginNodeId, nodeCreator,
nodeInitializer, OpenMayaMPx.MPxNode.kDeformerNode )
except:
sys.stderr.write( 'Failed to register node: ' + kPluginNodeName )
raise
def uninitializePlugin( mobject ):
''' Uninitializes the plug-in '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.deregisterNode( kPluginNodeId )
except:
sys.stderr.write( 'Failed to deregister node: ' + kPluginNodeName )
raise
##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:
import maya.cmds as cmds
cmds.loadPlugin( 'boundingBoxDeformer.py' )
cmds.polySphere()
cmds.deformer( type='boundingBoxDeformer' )
'''