├── .gitignore ├── README.md └── plugins ├── deformerTemplate.py ├── demoNode.py └── mnCollisionDeformer.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tutorials 2 | This is a repository for code to supplement video tutorials on my blog: https://mariekevanneutigem.nl/blog 3 | These plugins have been tested and written for maya 2019 so I can not guarantee behaviour will be the same in other versions. 4 | 5 | - https://www.artstation.com/mvn882/blog/pL67/writing-a-basic-maya-plugin-in-python 6 | - demoPlugin.py 7 | - https://www.artstation.com/mvn882/blog/bL6m/writing-a-basic-deformer-for-maya-in-python 8 | - deformerTemplate.py 9 | - mnCollisionDeformer.py 10 | -------------------------------------------------------------------------------- /plugins/deformerTemplate.py: -------------------------------------------------------------------------------- 1 | """deformerTemplate.py. 2 | 3 | Copyright (C) 2020 Marieke van Neutigem 4 | 5 | This code was written for educational purposes, it was written with the intent 6 | of learning and educating about writing deformers for maya. 7 | 8 | Contact: mvn882@hotmail.com 9 | https://mariekevanneutigem.nl/blog 10 | """ 11 | 12 | # You have to use maya API 1.0 because MPxDeformerNode is not available in 2.0. 13 | import maya.OpenMaya as OpenMaya 14 | import maya.OpenMayaMPx as OpenMayaMPx 15 | from maya.mel import eval as mel_eval 16 | 17 | 18 | # Set globals to the proper cpp cvars. (compatible from maya 2016) 19 | kInput = OpenMayaMPx.cvar.MPxGeometryFilter_input 20 | kInputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom 21 | kOutputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom 22 | kEnvelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope 23 | kGroupId = OpenMayaMPx.cvar.MPxGeometryFilter_groupId 24 | 25 | class templateDeformer(OpenMayaMPx.MPxDeformerNode): 26 | """Template deformer node.""" 27 | # Replace this with a valid node id for use in production. 28 | type_id = OpenMaya.MTypeId(0x00001) 29 | type_name = "templateDeformer" 30 | 31 | # Add attribute variables here. 32 | 33 | @classmethod 34 | def initialize(cls): 35 | """Initialize attributes and dependencies.""" 36 | # Add any input and outputs to the deformer here, also set up 37 | # dependencies between the in and outputs. If you want to use another 38 | # mesh as an input you can use an MFnGenericAttribute and add 39 | # MFnData.kMesh with the addDataAccept method. 40 | pass 41 | 42 | @classmethod 43 | def creator(cls): 44 | """Create instance of this class. 45 | 46 | Returns: 47 | templateDeformer: New class instance. 48 | """ 49 | return cls() 50 | 51 | def __init__(self): 52 | """Construction.""" 53 | OpenMayaMPx.MPxDeformerNode.__init__(self) 54 | 55 | def deform( 56 | self, 57 | data_block, 58 | geometry_iterator, 59 | local_to_world_matrix, 60 | geometry_index 61 | ): 62 | """Deform each vertex using the geometry iterator. 63 | 64 | Args: 65 | data_block (MDataBlock): the node's datablock. 66 | geometry_iterator (MItGeometry): 67 | iterator for the geometry being deformed. 68 | local_to_world_matrix (MMatrix): 69 | the geometry's world space transformation matrix. 70 | geometry_index (int): 71 | the index corresponding to the requested output geometry. 72 | """ 73 | # This is where you can add your deformation logic. 74 | 75 | # you can access the mesh this deformer is applied to either through 76 | # the given geometry_iterator, or by using the getDeformerInputGeometry 77 | # method below. 78 | 79 | # You can access all your defined attributes the way you would in any 80 | # other plugin, you can access base deformer attributes like the 81 | # envelope using the global variables like so: 82 | 83 | # envelope_attribute = kEnvelope 84 | # envelope_value = data_block.inputValue( envelope_attribute ).asFloat() 85 | 86 | def getDeformerInputGeometry(self, data_block, geometry_index): 87 | """Obtain a reference to the input mesh. 88 | 89 | We use MDataBlock.outputArrayValue() to avoid having to recompute the 90 | mesh and propagate this recomputation throughout the Dependency Graph. 91 | 92 | OpenMayaMPx.cvar.MPxGeometryFilter_input and 93 | OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom (Maya 2016) 94 | are SWIG-generated variables which respectively contain references to 95 | the deformer's 'input' attribute and 'inputGeom' attribute. 96 | 97 | Args: 98 | data_block (MDataBlock): the node's datablock. 99 | geometry_index (int): 100 | the index corresponding to the requested output geometry. 101 | """ 102 | inputAttribute = OpenMayaMPx.cvar.MPxGeometryFilter_input 103 | inputGeometryAttribute = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom 104 | 105 | inputHandle = data_block.outputArrayValue( inputAttribute ) 106 | inputHandle.jumpToElement( geometry_index ) 107 | inputGeometryObject = inputHandle.outputValue().child( 108 | inputGeometryAttribute 109 | ).asMesh() 110 | 111 | return inputGeometryObject 112 | 113 | 114 | def initializePlugin(plugin): 115 | """Called when plugin is loaded. 116 | 117 | Args: 118 | plugin (MObject): The plugin. 119 | """ 120 | plugin_fn = OpenMayaMPx.MFnPlugin(plugin) 121 | 122 | try: 123 | plugin_fn.registerNode( 124 | templateDeformer.type_name, 125 | templateDeformer.type_id, 126 | templateDeformer.creator, 127 | templateDeformer.initialize, 128 | OpenMayaMPx.MPxNode.kDeformerNode 129 | ) 130 | except: 131 | print "failed to register node {0}".format(templateDeformer.type_name) 132 | raise 133 | 134 | # Load custom Attribute Editor GUI. 135 | mel_eval( gui_template ) 136 | 137 | 138 | def uninitializePlugin(plugin): 139 | """Called when plugin is unloaded. 140 | 141 | Args: 142 | plugin (MObject): The plugin. 143 | """ 144 | plugin_fn = OpenMayaMPx.MFnPlugin(plugin, "Marieke van Neutigem", "0.0.1") 145 | 146 | try: 147 | plugin_fn.deregisterNode(templateDeformer.type_id) 148 | except: 149 | print "failed to deregister node {0}".format( 150 | templateDeformer.type_name 151 | ) 152 | raise 153 | 154 | 155 | # This is a custom attribute editor gui template, if you want to display your 156 | # attributes in a specific way you can define that here. (this is mel code) 157 | gui_template = ''' 158 | global proc AEmntemplateDeformerTemplate( string $nodeName ) 159 | { 160 | editorTemplate -beginScrollLayout; 161 | // Add attributes to show in attribute editor. 162 | editorTemplate -beginLayout "template Deformer Attributes" -collapse 0; 163 | // Add your own attributes here in the way you want them to be displayed. 164 | editorTemplate -endLayout; 165 | // Add base node attributes 166 | AEdependNodeTemplate $nodeName; 167 | // Add extra atttributes 168 | editorTemplate -addExtraControls; 169 | editorTemplate -endScrollLayout; 170 | } 171 | ''' 172 | -------------------------------------------------------------------------------- /plugins/demoNode.py: -------------------------------------------------------------------------------- 1 | """deformerTemplate.py. 2 | 3 | Copyright (C) 2020 Marieke van Neutigem 4 | 5 | This code was written for educational purposes, it was written with the intent 6 | of learning and educating about writing plugins for maya. 7 | 8 | To test this plugin in maya: 9 | 1. load the plugin using the plug-in manager. 10 | 3. Run this snippet in a python script editor tab: 11 | 12 | ------------------------------------snippet------------------------------------- 13 | from maya import cmds 14 | cmds.createNode('demoNode') 15 | ----------------------------------end snippet----------------------------------- 16 | 17 | Contact: mvn882@hotmail.com 18 | https://mariekevanneutigem.nl/blog 19 | """ 20 | 21 | from maya import cmds 22 | from maya.api import OpenMaya 23 | 24 | maya_useNewAPI = True 25 | 26 | class demoNode(OpenMaya.MPxNode): 27 | """A very simple demo node.""" 28 | # Maya provides the node ids from 29 | # 0x00000000 to 0x0007ffff 30 | # for users to customize. 31 | type_id = OpenMaya.MTypeId(0x00000000) 32 | type_name = "demoNode" 33 | 34 | # attributes 35 | input_one = None 36 | input_two = None 37 | output = None 38 | 39 | def __init__(self): 40 | OpenMaya.MPxNode.__init__(self) 41 | 42 | @classmethod 43 | def initialize(cls): 44 | """Create attributes and dependecies.""" 45 | numeric_attr = OpenMaya.MFnNumericAttribute() 46 | 47 | cls.input_one = numeric_attr.create( 48 | 'inputOne', # longname 49 | 'io', # shortname 50 | OpenMaya.MFnNumericData.kFloat # attribute type 51 | ) 52 | numeric_attr.readable = False 53 | numeric_attr.writable = True 54 | numeric_attr.keyable = True 55 | cls.addAttribute(cls.input_one) 56 | 57 | cls.input_two = numeric_attr.create( 58 | 'inputTwo', # longname 59 | 'it', # shortname 60 | OpenMaya.MFnNumericData.kFloat # attribute type 61 | ) 62 | numeric_attr.readable = False 63 | numeric_attr.writable = True 64 | numeric_attr.keyable = True 65 | cls.addAttribute(cls.input_two) 66 | 67 | cls.output = numeric_attr.create( 68 | 'output', # longname 69 | 'o', # shortname 70 | OpenMaya.MFnNumericData.kFloat # attribute type 71 | ) 72 | numeric_attr.readable = True 73 | numeric_attr.writable = False 74 | cls.addAttribute(cls.output) 75 | 76 | # attribute dependencies 77 | cls.attributeAffects( cls.input_one, cls.output ) 78 | cls.attributeAffects( cls.input_two, cls.output ) 79 | 80 | @classmethod 81 | def creator(cls): 82 | """Create class instance. 83 | 84 | Returns: 85 | demoNode: instance of this class. 86 | """ 87 | return cls() 88 | 89 | def compute(self, plug, data_block): 90 | """Compute this node. 91 | 92 | Args: 93 | plug (MPlug): 94 | plug representing the attribute that needs to be recomputed. 95 | data_block (MDataBlock): 96 | data block containing storage for the node's attributes. 97 | """ 98 | 99 | if plug == self.output: 100 | # get data from inputs 101 | input_one = data_block.inputValue(self.input_one).asFloat() 102 | input_two = data_block.inputValue(self.input_two).asFloat() 103 | 104 | # get output handle, set its new value, and set it clean. 105 | output_handle = data_block.outputValue(self.output) 106 | output_handle.setFloat((input_one + input_two)/2.0) 107 | output_handle.setClean() 108 | 109 | 110 | def initializePlugin(plugin): 111 | """Called when plugin is loaded. 112 | 113 | Args: 114 | plugin (MObject): The plugin. 115 | """ 116 | plugin_fn = OpenMaya.MFnPlugin(plugin, "Marieke van Neutigem", "0.0.1") 117 | 118 | try: 119 | plugin_fn.registerNode( 120 | demoNode.type_name, 121 | demoNode.type_id, 122 | demoNode.creator, 123 | demoNode.initialize, 124 | OpenMaya.MPxNode.kDependNode 125 | ) 126 | except: 127 | print "failed to register node {0}".format(demoNode.type_name) 128 | raise 129 | 130 | 131 | def uninitializePlugin(plugin): 132 | """Called when plugin is unloaded. 133 | 134 | Args: 135 | plugin (MObject): The plugin. 136 | """ 137 | plugin_fn = OpenMaya.MFnPlugin(plugin) 138 | 139 | try: 140 | plugin_fn.deregisterNode(demoNode.type_id) 141 | except: 142 | print "failed to deregister node {0}".format(demoNode.type_name) 143 | raise 144 | -------------------------------------------------------------------------------- /plugins/mnCollisionDeformer.py: -------------------------------------------------------------------------------- 1 | """mnCollisionDeformer.py. 2 | 3 | Copyright (C) 2020 Marieke van Neutigem 4 | 5 | This plugin was written for educational purposes, it was written with the intent 6 | of learning and educating about writing deformers for maya. 7 | 8 | To test this plugin in maya: 9 | 1. load the plugin using the plug-in manager. 10 | 2. Select the affected mesh and the collider, in that order. 11 | 3. Run this snippet in a python script editor tab: 12 | 13 | ------------------------------------snippet------------------------------------- 14 | from maya import cmds 15 | selection = cmds.ls(sl=True) 16 | if len(selection) == 2: 17 | mesh = selection[0] 18 | collider_shapes = cmds.listRelatives(selection[1], shapes=True) 19 | cmds.select(mesh) 20 | deformer_nodes = cmds.deformer( type='mnCollisionDeformer' ) 21 | cmds.connectAttr( 22 | '{0}.worldMesh'.format(collider_shapes[0]), 23 | '{0}.collider'.format(deformer_nodes[0]), 24 | ) 25 | else: 26 | print 'Failed to add mnCollisionDeformer, please select mesh and collider.' 27 | ----------------------------------end snippet----------------------------------- 28 | 29 | Contact: mvn882@hotmail.com 30 | https://mariekevanneutigem.nl/blog 31 | """ 32 | import math 33 | 34 | # Use maya API 1.0 because MPxDeformerNode is not available in 2.0 yet. 35 | import maya.OpenMaya as OpenMaya 36 | import maya.OpenMayaMPx as OpenMayaMPx 37 | from maya.mel import eval as mel_eval 38 | 39 | 40 | # set globals to the proper cpp cvars. (compatible from maya 2016) 41 | kInput = OpenMayaMPx.cvar.MPxGeometryFilter_input 42 | kInputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom 43 | kOutputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom 44 | kEnvelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope 45 | kGroupId = OpenMayaMPx.cvar.MPxGeometryFilter_groupId 46 | 47 | class mnCollisionDeformer(OpenMayaMPx.MPxDeformerNode): 48 | """Node to deform mesh on collision.""" 49 | # replace this with a valid node id for use in production. 50 | type_id = OpenMaya.MTypeId(0x00001) 51 | type_name = "mnCollisionDeformer" 52 | 53 | collider_attr = None 54 | bulge_multiplier_attr = None 55 | levels_attr = None 56 | bulgeshape_attr = None 57 | 58 | @classmethod 59 | def initialize(cls): 60 | """Create attributes.""" 61 | numeric_attr_fn = OpenMaya.MFnNumericAttribute() 62 | generic_attr_fn = OpenMaya.MFnGenericAttribute() 63 | ramp_attr_fn = OpenMaya.MRampAttribute() 64 | 65 | # Collider mesh as an input, this needs to be connected to the worldMesh 66 | # output attribute on a given shape. 67 | cls.collider_attr = generic_attr_fn.create( 68 | 'collider', 69 | 'cl', 70 | ) 71 | generic_attr_fn.addDataAccept( OpenMaya.MFnData.kMesh ) 72 | cls.addAttribute( cls.collider_attr ) 73 | 74 | # Multiplier for the amount of bulge to apply. 75 | cls.bulge_multiplier_attr = numeric_attr_fn.create( 76 | 'bulgeMultiplier', 77 | 'bm', 78 | OpenMaya.MFnNumericData.kFloat 79 | ) 80 | numeric_attr_fn.readable = False 81 | numeric_attr_fn.writable = True 82 | numeric_attr_fn.keyable = True 83 | cls.addAttribute(cls.bulge_multiplier_attr) 84 | 85 | # Levels of vertices to apply the bulge to. 86 | cls.levels_attr = numeric_attr_fn.create( 87 | 'levels', 88 | 'l', 89 | OpenMaya.MFnNumericData.kInt 90 | ) 91 | numeric_attr_fn.readable = False 92 | numeric_attr_fn.writable = True 93 | numeric_attr_fn.keyable = True 94 | cls.addAttribute(cls.levels_attr) 95 | 96 | # Shape of the bulge, as a ramp to be user directable. 97 | cls.bulgeshape_attr = ramp_attr_fn.createCurveRamp( 98 | "bulgeShape", 99 | "bs" 100 | ) 101 | cls.addAttribute(cls.bulgeshape_attr) 102 | 103 | # All inputs affect the output geometry. 104 | cls.attributeAffects( cls.bulgeshape_attr, kOutputGeom ) 105 | cls.attributeAffects( cls.levels_attr, kOutputGeom ) 106 | cls.attributeAffects( cls.bulge_multiplier_attr, kOutputGeom ) 107 | cls.attributeAffects( cls.collider_attr, kOutputGeom ) 108 | 109 | @classmethod 110 | def creator(cls): 111 | """Create instance of this class. 112 | 113 | Returns: 114 | mnCollisionDeformer: New class instance. 115 | """ 116 | return cls() 117 | 118 | def __init__(self): 119 | """Construction.""" 120 | OpenMayaMPx.MPxDeformerNode.__init__(self) 121 | 122 | def postConstructor(self): 123 | """This is called when the node has been added to the scene.""" 124 | 125 | # Populate bulge shape ramp attribute with default values. 126 | node = self.thisMObject() 127 | bulgeshape_handle = OpenMaya.MRampAttribute(node, self.bulgeshape_attr) 128 | 129 | positions = OpenMaya.MFloatArray() 130 | values = OpenMaya.MFloatArray() 131 | interps = OpenMaya.MIntArray() 132 | 133 | positions.append(float(0.0)) 134 | positions.append(float(1.0)) 135 | 136 | values.append(float(0.0)) 137 | values.append(float(1.0)) 138 | 139 | interps.append(OpenMaya.MRampAttribute.kSpline) 140 | interps.append(OpenMaya.MRampAttribute.kSpline) 141 | 142 | bulgeshape_handle.addEntries(positions, values, interps) 143 | 144 | 145 | def deform( 146 | self, 147 | data_block, 148 | geometry_iterator, 149 | local_to_world_matrix, 150 | geometry_index 151 | ): 152 | """Deform each vertex using the geometry iterator. 153 | 154 | Args: 155 | data_block (MDataBlock): the node's datablock. 156 | geometry_iterator (MItGeometry): 157 | iterator for the geometry being deformed. 158 | local_to_world_matrix (MMatrix): 159 | the geometry's world space transformation matrix. 160 | geometry_index (int): 161 | the index corresponding to the requested output geometry. 162 | """ 163 | # The envelope determines the weight of the deformer on the mesh. 164 | envelope_attribute = kEnvelope 165 | envelope_value = data_block.inputValue( envelope_attribute ).asFloat() 166 | 167 | # Get the input mesh from the datablock using our 168 | # getDeformerInputGeometry() helper function. 169 | input_geometry_object = self.getDeformerInputGeometry( 170 | data_block, 171 | geometry_index 172 | ) 173 | 174 | # Get the collider mesh, abort if none is found. 175 | collider_handle = data_block.inputValue( self.collider_attr ) 176 | try: 177 | collider_object = collider_handle.asMesh() 178 | collider_fn = OpenMaya.MFnMesh(collider_object) 179 | except: 180 | return 181 | 182 | # Obtain the list of normals for each vertex in the mesh. 183 | normals = OpenMaya.MFloatVectorArray() 184 | mesh_fn = OpenMaya.MFnMesh( input_geometry_object ) 185 | mesh_fn.getVertexNormals( True, normals, OpenMaya.MSpace.kTransform ) 186 | 187 | # Store the original points of this mesh, if all points turn out to be 188 | # inside the collider we will want to use this to restore the original 189 | # points instead of using the overrides. 190 | orig_points = OpenMaya.MPointArray() 191 | mesh_fn.getPoints(orig_points) 192 | 193 | mesh_vertex_iterator = OpenMaya.MItMeshVertex(input_geometry_object) 194 | 195 | # Iterate over the vertices to move them. 196 | global vertexIncrement 197 | intersecting_indices = [] 198 | neighbouring_indices = [] 199 | 200 | inside_mesh = True 201 | # denting the mesh inwards along the collider. 202 | while not mesh_vertex_iterator.isDone(): 203 | 204 | vertex_index = mesh_vertex_iterator.index() 205 | 206 | normal = OpenMaya.MVector( normals[vertex_index] ) 207 | 208 | # Get the world space point/normal in float and non float values. 209 | point = mesh_vertex_iterator.position() 210 | ws_point = point * local_to_world_matrix 211 | ws_fl_point = OpenMaya.MFloatPoint(ws_point) 212 | 213 | ws_normal = normal * local_to_world_matrix 214 | # inverting the direction of the normal to make it point in the same 215 | # direction as the colliding mesh's normal. 216 | ws_fl_normal = OpenMaya.MFloatVector(ws_normal) * -1 217 | 218 | # Get the intersection for this point/normal combination. 219 | intersecting_point = self.getIntersection( 220 | ws_fl_point, 221 | ws_fl_normal, 222 | collider_fn 223 | ) 224 | 225 | # if no intersecting point is found skip it. 226 | if intersecting_point: 227 | # get the vector from the intersecting point to the 228 | # original point 229 | diff = intersecting_point - ws_fl_point 230 | 231 | # transform the vector to local space, and multiply it using 232 | # the given envelope value to determine the influence. 233 | new_point = point + OpenMaya.MVector( 234 | diff * envelope_value 235 | ) * local_to_world_matrix.inverse() 236 | 237 | # get connected vertices of this vertex, store them in the 238 | # neighbouring indices list to use later on to create the 239 | # outwards bulging. 240 | verts = OpenMaya.MIntArray() 241 | mesh_vertex_iterator.getConnectedVertices(verts) 242 | for i in range(verts.length()): 243 | neighbouring_indices.append(verts[i]) 244 | 245 | # Set the position of the current vertex to the new point. 246 | mesh_vertex_iterator.setPosition( new_point ) 247 | 248 | # store this point as an intersecting index. 249 | intersecting_indices.append(vertex_index) 250 | else: 251 | inside_mesh = False 252 | 253 | # Jump to the next vertex. 254 | mesh_vertex_iterator.next() 255 | 256 | # get the bulge and levels values. 257 | bulge = data_block.inputValue(self.bulge_multiplier_attr).asFloat() 258 | levels = data_block.inputValue(self.levels_attr).asInt() 259 | if inside_mesh: 260 | mesh_fn.setPoints(orig_points) 261 | elif levels and bulge: 262 | # dent the mesh outward according to user input variables. 263 | bulgeshape_handle = OpenMaya.MRampAttribute( 264 | self.thisMObject(), 265 | self.bulgeshape_attr 266 | ) 267 | # get the list of neighbourhing indices that arent part of the 268 | # intersecting indices. These will be used to identify what vertices 269 | # to bulge outwards. 270 | outer_neighbour_indices = list( 271 | set(neighbouring_indices) - set(intersecting_indices) 272 | ) 273 | multiplier = bulge * envelope_value 274 | 275 | # This is a recrusive method and will continue on for a given amount 276 | # of "levels" of depth. 277 | self.deformNeighbours( 278 | mesh_vertex_iterator, 279 | local_to_world_matrix, 280 | normals, 281 | intersecting_indices, 282 | outer_neighbour_indices, 283 | collider_fn, 284 | bulgeshape_handle, 285 | levels, 286 | multiplier, 287 | levels 288 | ) 289 | 290 | def deformNeighbours( 291 | self, 292 | mesh_vertex_iterator, 293 | local_to_world_matrix, 294 | normals, 295 | past_indices, 296 | indices, 297 | collider_fn, 298 | bulgeshape_handle = None, 299 | levels = 1, 300 | multiplier = 1.0, 301 | max_levels = 1 302 | ): 303 | """Deform the given indices using given arguments. 304 | 305 | This is a recursive method, it will continue to find neighbouring 306 | indices and execute this method on them for a given amount of levels. 307 | 308 | Due to this the mesh density has a big influence on the way the out 309 | dent is shaped, it will likely be more performant to replace this logic 310 | by using distance based rather than neighbourhing logic though this will 311 | affect the look of the bulge. 312 | 313 | Args: 314 | mesh_vertex_iterator (MItMeshVertex): mesh iterator for the original 315 | geometry, passed by reference so changes made to this will 316 | be reflected live. 317 | local_to_world_matrix (MMatrix): transformation matrix to transform 318 | given mesh vertex iterator data to world space. 319 | normals (MFloatVectorArray): array of normals by index. 320 | past_indices (list): indices to skip over, used to calculate new 321 | list of indices for recurisve logic. 322 | indices (list): list of indices to apply the deformation to. 323 | collider_fn (MFnMesh): mesh of the object the mesh vertices are 324 | colliding with. 325 | bulgeshape_handle (MDataHandle): handle of the bulgeshape ramp. 326 | levels (int): current number of levels of recursion, also used as to 327 | map the value from the bulgeshape ramp. 328 | mutliplier (float): value to multiply strength of deformation with. 329 | max_levels(int): total number of recursions. 330 | """ 331 | 332 | # Calculate the amount to bulge this layer of vertices. 333 | bulge_amount = None 334 | if bulgeshape_handle: 335 | # get the value for the current level from the ramp. 336 | bulgeshape_util = OpenMaya.MScriptUtil() 337 | bulgeshape = bulgeshape_util.asFloatPtr() 338 | try: 339 | bulgeshape_handle.getValueAtPosition( 340 | float(levels)/float(max_levels), 341 | bulgeshape 342 | ) 343 | except: 344 | bulgeshape = None 345 | if bulgeshape: 346 | bulge_amount = OpenMaya.MScriptUtil().getFloat(bulgeshape) 347 | 348 | # If it failed to get current bulge amount from ramp then fall back to 349 | # an exponential curve. 350 | if not bulge_amount: 351 | bulge_amount = math.pow(levels, 2) / max_levels 352 | 353 | # Iterate all indices and apply the deformation. 354 | neighbouring_indices = [] 355 | for i in indices: 356 | # throwaway script util because setIndex needs an int pointer. 357 | util = OpenMaya.MScriptUtil() 358 | prev_index = util.asIntPtr() 359 | mesh_vertex_iterator.setIndex(i, prev_index) 360 | 361 | # Get the world space point/normal in float and non float values. 362 | point = mesh_vertex_iterator.position() 363 | ws_point = point * local_to_world_matrix 364 | ws_fl_point = OpenMaya.MFloatPoint(ws_point) 365 | 366 | normal = OpenMaya.MVector( normals[i] ) 367 | ws_normal = normal * local_to_world_matrix 368 | ws_fl_normal = OpenMaya.MFloatVector(ws_normal) 369 | 370 | # Get the closest intersection along the normal. 371 | intersections = OpenMaya.MFloatPointArray() 372 | collider_fn.allIntersections( 373 | ws_fl_point, ws_fl_normal, None, None, False, OpenMaya.MSpace.kWorld, 374 | 1000, False, None, True, intersections, None, 375 | None, None, None, None 376 | ) 377 | # Get the closest point by relying on the ordered array. 378 | intersecting_point = None 379 | if intersections.length() > 0: 380 | intersecting_point = intersections[0] 381 | 382 | # calculate the offset vector to add to the point. 383 | offset_vector = normal * multiplier * bulge_amount 384 | 385 | # Cap the length of the bulge to prevent the bulge from clipping 386 | # through the collider. 387 | if intersecting_point: 388 | diff = OpenMaya.MVector( intersecting_point - ws_fl_point ) 389 | if diff.length() < offset_vector.length(): 390 | offset_vector = diff * local_to_world_matrix.inverse() 391 | 392 | # calculate and set position of deformed point. 393 | new_point = point + offset_vector 394 | mesh_vertex_iterator.setPosition( new_point ) 395 | 396 | # get connected vertices of this vertex, store them in the 397 | # neighbouring indices list to use later on to create the 398 | # outwards bulging. 399 | verts = OpenMaya.MIntArray() 400 | mesh_vertex_iterator.getConnectedVertices(verts) 401 | for i in range(verts.length()): 402 | neighbouring_indices.append(verts[i]) 403 | 404 | # If the current level is not 0, continue recursion. 405 | levels = levels - 1 406 | if levels > 0: 407 | # get the list of neighbourhing indices that arent part of the 408 | # past indices. These will be used to identify what vertices 409 | # to bulge outwards next. 410 | past_indices.extend(indices) 411 | new_indices = list( 412 | set(neighbouring_indices) - set(past_indices) 413 | ) 414 | self.deformNeighbours( 415 | mesh_vertex_iterator, 416 | local_to_world_matrix, 417 | normals, 418 | past_indices, 419 | new_indices, 420 | collider_fn, 421 | bulgeshape_handle, 422 | levels, 423 | multiplier, 424 | max_levels 425 | ) 426 | 427 | 428 | def getIntersection(self, point, normal, mesh): 429 | """Get the "best" intersection to move point to on given mesh. 430 | 431 | Args: 432 | point (MFloatPoint): point to check if inside mesh. 433 | normal (MFloatVector): inverted normal of given point. 434 | mesh (MFnMesh): mesh to check if point inside. 435 | 436 | Returns: 437 | MPoint 438 | """ 439 | intersection_normal = OpenMaya.MVector() 440 | closest_point = OpenMaya.MPoint() 441 | 442 | # Get closest point/normal to given point in normal direction on mesh. 443 | mesh.getClosestPointAndNormal( 444 | OpenMaya.MPoint(point), 445 | closest_point, 446 | intersection_normal, 447 | OpenMaya.MSpace.kWorld, 448 | ) 449 | 450 | # if the the found normal on the mesh is in a direction opposite to the 451 | # given normal, fall back to given normal, else use the average normal. 452 | # This is to get a more even vertex distribution on the new mesh. 453 | angle = normal.angle(OpenMaya.MFloatVector(intersection_normal)) 454 | if angle >= math.pi or angle <= -math.pi: 455 | average_normal = normal 456 | else: 457 | average_normal = OpenMaya.MVector(normal) + intersection_normal 458 | 459 | # Find intersection in direction determined above. 460 | intersections = OpenMaya.MFloatPointArray() 461 | mesh.allIntersections( 462 | point, OpenMaya.MFloatVector(average_normal), None, None, False, OpenMaya.MSpace.kWorld, 463 | 1000, False, None, True, intersections, None, 464 | None, None, None, None 465 | ) 466 | 467 | # If number of intersections is even then the given point is not inside 468 | # the mesh. The intersections are ordered so return the first one found 469 | # as that is the closest one. 470 | intersecting_point = None 471 | if intersections.length()%2 == 1: 472 | intersecting_point = intersections[0] 473 | 474 | return intersecting_point 475 | 476 | 477 | def getDeformerInputGeometry(self, data_block, geometry_index): 478 | """Obtain a reference to the input mesh. 479 | 480 | We use MDataBlock.outputArrayValue() to avoid having to recompute the 481 | mesh and propagate this recomputation throughout the Dependency Graph. 482 | 483 | OpenMayaMPx.cvar.MPxGeometryFilter_input and 484 | OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom (Maya 2016) 485 | are SWIG-generated variables which respectively contain references to 486 | the deformer's 'input' attribute and 'inputGeom' attribute. 487 | 488 | Args: 489 | data_block (MDataBlock): the node's datablock. 490 | geometry_index (int): 491 | the index corresponding to the requested output geometry. 492 | """ 493 | inputAttribute = OpenMayaMPx.cvar.MPxGeometryFilter_input 494 | inputGeometryAttribute = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom 495 | 496 | inputHandle = data_block.outputArrayValue( inputAttribute ) 497 | inputHandle.jumpToElement( geometry_index ) 498 | inputGeometryObject = inputHandle.outputValue().child( 499 | inputGeometryAttribute 500 | ).asMesh() 501 | 502 | return inputGeometryObject 503 | 504 | 505 | def initializePlugin(plugin): 506 | """Called when plugin is loaded. 507 | 508 | Args: 509 | plugin (MObject): The plugin. 510 | """ 511 | plugin_fn = OpenMayaMPx.MFnPlugin(plugin, "Marieke van Neutigem", "0.0.1") 512 | 513 | try: 514 | plugin_fn.registerNode( 515 | mnCollisionDeformer.type_name, 516 | mnCollisionDeformer.type_id, 517 | mnCollisionDeformer.creator, 518 | mnCollisionDeformer.initialize, 519 | OpenMayaMPx.MPxNode.kDeformerNode 520 | ) 521 | except: 522 | print "failed to register node {0}".format(mnCollisionDeformer.type_name) 523 | raise 524 | 525 | # Load custom Attribute Editor GUI. 526 | mel_eval( gui_template ) 527 | 528 | 529 | def uninitializePlugin(plugin): 530 | """Called when plugin is unloaded. 531 | 532 | Args: 533 | plugin (MObject): The plugin. 534 | """ 535 | plugin_fn = OpenMayaMPx.MFnPlugin(plugin) 536 | 537 | try: 538 | plugin_fn.deregisterNode(mnCollisionDeformer.type_id) 539 | except: 540 | print "failed to deregister node {0}".format( 541 | mnCollisionDeformer.type_name 542 | ) 543 | raise 544 | 545 | 546 | # Custom attribute editor gui template 547 | gui_template = ''' 548 | global proc AEmnCollisionDeformerTemplate( string $nodeName ) 549 | { 550 | editorTemplate -beginScrollLayout; 551 | // Add attributes to show in attribute editor. 552 | editorTemplate -beginLayout "Collision Deformer Attributes" -collapse 0; 553 | editorTemplate -addSeparator; 554 | editorTemplate -addControl "collider" ; 555 | editorTemplate -addControl "levels" ; 556 | editorTemplate -addControl "bulgeMultiplier" ; 557 | editorTemplate -addControl "envelope" ; 558 | AEaddRampControl "bulgeShape" ; 559 | editorTemplate -endLayout; 560 | // Add base node attributes 561 | AEdependNodeTemplate $nodeName; 562 | // Add extra atttributes 563 | editorTemplate -addExtraControls; 564 | editorTemplate -endScrollLayout; 565 | } 566 | ''' 567 | --------------------------------------------------------------------------------