Tuesday, 6 June 2017

Collision Based Deformer

This post briefly details three examples of a deformer that can react to collisions with another object. The end video shows all three examples in action and as ever a basic python version of the compiled plugin is included to get started with.

Direct deformation
The first example is the most basic implementation of the node to achieve direct deformation.
Using the MFnMesh::allIntersections to detect intersection between two meshes and extracting and applying the delta between the intersecting points it is possible to create an effect of direct deformation.
It is worth noting that allIntersections has some caveats.
- The first is that any mesh that you are working with must be a closed surface. This is because it calculates collision by firing a ray from a given point and calculates how many surfaces it has passed through before it dies. If it passes through one it must be inside a mesh, if two it must be outside. An open mesh has the risk of only having one hit even if the point is inside the mesh.
- The second is that as all deltas are obtained by returning the closest point on the collision objects surface from a given point on the colliding object it is possible that the returned closest point might be on the opposite side of the collision object. This is because the colliding object has travelled past a centre line switching where the closest point will now be. This will give the result of vertices snapping to the wrong side of a mesh although the effect can be quite interesting.

Secondary deformation
The second example expands on the first and adds secondary deformation. This version retains all the features of the first but also pushes the intersecting vertex out along its normal to give an idea of volume retention. This is adjustable so that the result can be extended or switched off alltogether. This deformer gives control of the falloff shape using an MRampAttribute and an attribute to define how much of the surface the effect covers. it is also possible to paint its attributes to have fine control over the end result.

Sticky deformation
The third example changes direction and stores all colliding deformed points in an array only updating their position if their delta increases. Added to this is a compute based timer that gradually returns the mesh back to its original shape unless collided with again.

Collision Based Deformer from SBGrover on Vimeo.

Below is a python implementation of the first example to get started with. This will give you the direct deformation. Be aware that as this is using Python the results are much slower than a compiled plugin so it is best not to throw this at dense geometry. Included is a helper function to build a scene with the plugin.


 import maya.OpenMaya as OpenMaya  
 import maya.OpenMayaAnim as OpenMayaAnim  
 import maya.OpenMayaMPx as OpenMayaMPx  
 class collisionDeformer(OpenMayaMPx.MPxDeformerNode):  
      kPluginNodeId = OpenMaya.MTypeId(0x00000012)  
      kPluginNodeTypeName = "collisionDeformer"  
      def __init__(self):  
           OpenMayaMPx.MPxDeformerNode.__init__( self )  
           self.accelParams = OpenMaya.MMeshIsectAccelParams() #speeds up intersect calculation  
           self.intersector = OpenMaya.MMeshIntersector() #contains methods for efficiently finding the closest point to a mesh, required for collider  
      def deform( self, block, geoItr, matrix, index ):  
           #get ENVELOPE  
           envelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope  
           envelopeHandle = block.inputValue(envelope)  
           envelopeVal = envelopeHandle.asFloat()  
           if envelopeVal!=0:  
                #get COLLIDER MESH (as worldMesh)  
                colliderHandle = block.inputValue(self.collider)  
                inColliderMesh = colliderHandle.asMesh()  
                if not inColliderMesh.isNull():  
                     #get collider fn mesh  
                     inColliderFn = OpenMaya.MFnMesh(inColliderMesh)  
                     #get DEFORMED MESH  
                     inMesh = self.get_input_geom(block, index)  
                     #get COLLIDER WORLD MATRIX to convert the bounding box to world space  
                     colliderMatrixHandle = block.inputValue(self.colliderMatrix)  
                     colliderMatrixVal = colliderMatrixHandle.asMatrix()  
                     #get BOUNDING BOX MIN VALUES  
                     colliderBoundingBoxMinHandle = block.inputValue(self.colliderBoundingBoxMin)  
                     colliderBoundingBoxMinVal = colliderBoundingBoxMinHandle.asFloat3()  
                     #get BOUNDING BOX MAX VALUES  
                     colliderBoundingBoxMaxHandle = block.inputValue(self.colliderBoundingBoxMax)  
                     colliderBoundingBoxMaxVal = colliderBoundingBoxMaxHandle.asFloat3()  
                     #build new bounding box based on given values  
                     bbox = OpenMaya.MBoundingBox()  
                     bbox.expand(OpenMaya.MPoint(colliderBoundingBoxMinVal[0], colliderBoundingBoxMinVal[1], colliderBoundingBoxMinVal[2]))  
                     bbox.expand(OpenMaya.MPoint(colliderBoundingBoxMaxVal[0], colliderBoundingBoxMaxVal[1], colliderBoundingBoxMaxVal[2]))  
                     #set up point on mesh and intersector for returning closest point and accelParams if required  
                     pointOnMesh = OpenMaya.MPointOnMesh()   
                     self.intersector.create(inColliderMesh, colliderMatrixVal)  
                     #set up constants for allIntersections  
                     faceIds = None  
                     triIds = None  
                     idsSorted = False  
                     space = OpenMaya.MSpace.kWorld  
                     maxParam = 100000  
                     testBothDirs = False  
                     accelParams = None  
                     sortHits = False  
                     hitRayParams = None  
                     hitFaces = None  
                     hitTriangles = None  
                     hitBary1 = None  
                     hitBary2 = None  
                     tolerance = 0.0001  
                     floatVec = OpenMaya.MFloatVector(0, 1, 0) #set up arbitrary vector n.b this is fine for what we want here but anything more complex may require vector obtained from vertex  
                     #deal with main mesh  
                     inMeshFn = OpenMaya.MFnMesh(inMesh)  
                     inPointArray = OpenMaya.MPointArray()  
                     inMeshFn.getPoints(inPointArray, OpenMaya.MSpace.kWorld)  
                     #create array to store final points and set to correct length  
                     length = inPointArray.length()  
                     finalPositionArray = OpenMaya.MPointArray()  
                     #loop through all points. could also be done with geoItr  
                     for num in range(length):  
                          point = inPointArray[num]  
                          #if point is within collider bounding box then consider it  
                          if bbox.contains(point):  
                               ##-- allIntersections variables --##  
                               floatPoint = OpenMaya.MFloatPoint(point)  
                               hitPoints = OpenMaya.MFloatPointArray()  
                               inColliderFn.allIntersections( floatPoint, floatVec, faceIds, triIds, idsSorted, space, maxParam, testBothDirs, accelParams, sortHits, hitPoints, hitRayParams, hitFaces, hitTriangles, hitBary1, hitBary2, tolerance )  
                               if hitPoints.length()%2 == 1:       
                                    #work out closest point  
                                    closestPoint = OpenMaya.MPoint()  
                                    inColliderFn.getClosestPoint(point, closestPoint, OpenMaya.MSpace.kWorld, None)  
                                    #calculate delta and add to array  
                                    delta = point - closestPoint  
                                    finalPositionArray.set(point - delta, num)  
                                    finalPositionArray.set(point, num)  
                          #if point is not in bounding box simply add the position to the final array  
                               finalPositionArray.set(point, num)  
                     inMeshFn.setPoints(finalPositionArray, OpenMaya.MSpace.kWorld)  
      def get_input_geom(self, block, index):  
           input_attr = OpenMayaMPx.cvar.MPxGeometryFilter_input  
           input_geom_attr = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom  
           input_handle = block.outputArrayValue(input_attr)  
           input_geom_obj = input_handle.outputValue().child(input_geom_attr).asMesh()  
           return input_geom_obj  
 def creator():  
      return OpenMayaMPx.asMPxPtr(collisionDeformer())  
 def initialize():  
      gAttr = OpenMaya.MFnGenericAttribute()  
      mAttr = OpenMaya.MFnMatrixAttribute()  
      nAttr = OpenMaya.MFnNumericAttribute()  
      collisionDeformer.collider = gAttr.create( "colliderTarget", "col")  
      gAttr.addDataAccept( OpenMaya.MFnData.kMesh )  
      collisionDeformer.colliderBoundingBoxMin = nAttr.createPoint( "colliderBoundingBoxMin", "cbbmin")  
      collisionDeformer.colliderBoundingBoxMax = nAttr.createPoint( "colliderBoundingBoxMax", "cbbmax")  
      collisionDeformer.colliderMatrix = mAttr.create("colliderMatrix", "collMatr", OpenMaya.MFnNumericData.kFloat )  
      collisionDeformer.multiplier = nAttr.create("multiplier", "mult", OpenMaya.MFnNumericData.kFloat, 1)  
      collisionDeformer.addAttribute( collisionDeformer.collider )  
      collisionDeformer.addAttribute( collisionDeformer.colliderMatrix )  
      collisionDeformer.addAttribute( collisionDeformer.colliderBoundingBoxMin )  
      collisionDeformer.addAttribute( collisionDeformer.colliderBoundingBoxMax )  
      collisionDeformer.addAttribute( collisionDeformer.multiplier )  
      outMesh = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom  
      collisionDeformer.attributeAffects( collisionDeformer.collider, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.colliderBoundingBoxMin, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.colliderBoundingBoxMax, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.colliderMatrix, outMesh )  
      collisionDeformer.attributeAffects( collisionDeformer.multiplier, outMesh )  
 def initializePlugin(obj):  
      plugin = OpenMayaMPx.MFnPlugin(obj, 'Grover', '1.0', 'Any')  
           plugin.registerNode('collisionDeformer', collisionDeformer.kPluginNodeId, creator, initialize, OpenMayaMPx.MPxNode.kDeformerNode)  
           raise RuntimeError, 'Failed to register node'  
 def uninitializePlugin(obj):  
      plugin = OpenMayaMPx.MFnPlugin(obj)  
           raise RuntimeError, 'Failed to deregister node'  
 #simply create two polygon spheres. Move the second away from the first, select the first and run the code below.  
 import maya.cmds as cmds  
 cmds.connectAttr('pSphere2.worldMesh', 'collisionDeformer1.colliderTarget')  
 cmds.connectAttr('pSphere2.matrix', 'collisionDeformer1.colliderMatrix')  
 cmds.connectAttr('pSphere2.boundingBox.boundingBoxSize.boundingBoxSizeX', 'collisionDeformer1.colliderBoundingBox.colliderBoundingBoxX')  
 cmds.connectAttr('pSphere2.boundingBox.boundingBoxSize.boundingBoxSizeY', 'collisionDeformer1.colliderBoundingBox.colliderBoundingBoxY')  
 cmds.connectAttr('pSphere2.boundingBox.boundingBoxSize.boundingBoxSizeZ', 'collisionDeformer1.colliderBoundingBox.colliderBoundingBoxZ')  


Post a Comment