Friday, 4 August 2017

Stretch Compress Deformer

Although working in games limits me to joint and blendshape solutions to achieve reasonable levels of deformation on characters sometimes it's nice to take a departure from this and think a bit further afield.
A typical problem we have in our engine is that like many others it does not support joint scaling - either uniformly or non uniformly. This can be a bit of a challenge when trying to maintain volume in characters as something that could be driven by one scaling joint and some simple skinning has to end up being driven by three or four joints that translate away from each other. When time is critical it can be frustrating to set up basic stuff like this as it takes time to adjust the weighting and driving to give the right effect.
When dealing with driving, a pose space solution is generally relied on (at least where I work) to help drive these joints in the right manner. Setting this up takes time and can sometimes be broken when a character twists too far or away from the pose readers.

This is where the Stretch Compress Deformer could be of use.

This plugin is applied directly to a skinned mesh and it's result is entirely driven by the measured area of all polygons within the mesh rather than an external reader. Input target shapes give an example of the shape the mesh must achieve in the areas that compress or stretch. It can also be weighted so that only small areas are considered which will of course aid performance.
In approaching the plugin I knew that I would need to calculate the area of a polygon. I did not realise that MItMeshPolygon had its own function specifically for this. GetArea.
Instead I used Herons Formula although there are a number of ways of finding the result.
By storing the area of all triangles on the deformed mesh initially and then on each update comparing this original set to a new set it is possible to obtain a shortlist of triangles who's surface area has decreased - compression, and those that have increased - stretching. Converting those faces to vertices then means that the current shape can be adjusted to match that of the input target shapes based on a weight that can be controlled by the user.
Initially we will have also stored off the vertex positions from the bind (shape of mesh before deformation), target and stretch we can now obtain the deltas between thier corresponding vertices. By multiplying those deltas by the corresponding normal vector from the bind a scalar vector is obtained. Multiplying the deformed normal vector by this scalar before adding this result to the current point position pushes the deformed vertex inwards or outwards depending on triangle surface area.

EXAMPLE VIDEO

Stretch Compress Deformer from SBGrover on Vimeo.

I provide python code below. Note that this is not a version of the plugin I wrote but instead a python script example intended to be run in the script editor. As a result it does have certain caveats. The logic is exactly the same but it can only be run once on a mesh and if the mesh has been posed using joints then it will need to be unbound beforehand. The provided script is meant purely as an aid to learning and not as a complete solution to the problem. I leave it to you to push it further and convert it into a plugin.

To use the script:

1. Create a Base shape and a compressed and stretched version of the Base shape. The topology must match EXACTLY.
2. If you wish to, skin the Base shape to joints.
3. Select the Base, Stretch and Compress in that order.
4. Run the first part of the script.
5. Pose the Base shape either by moving the geometry or moving the joints.
6. Delete the history on the Base shape if it is skinned or has been adjusted using a deformer.
7. Run the second script.

SCRIPT PART 1 TO BE RUN ON BASE, STRETCH, COMPRESS

import maya.OpenMaya as om
import math

# Need: One compress and once stretch target and a skinned mesh in bind pose
# RUN THIS ONCE WITH MESH IN BIND POSE

# have the three meshes selected in the following order: bind, stretch, compress
sel = om.MSelectionList()
om.MGlobal.getActiveSelectionList(sel)

# bind
dag_path = om.MDagPath()
sel.getDagPath(0, dag_path)
bind_fn = om.MFnMesh(dag_path)

# stretch
sel.getDagPath(1, dag_path)
stretch_fn = om.MFnMesh(dag_path)
stretch_points = om.MPointArray()
stretch_fn.getPoints(stretch_points, om.MSpace.kObject)

# compress
sel.getDagPath(2, dag_path)
compress_fn = om.MFnMesh(dag_path)
compress_points = om.MPointArray()
compress_fn.getPoints(compress_points, om.MSpace.kObject)

# variables
overall_weight = 2 # change this to increase / decrease the overall effect
compress_weight = 5 # change this to increase / decrease the compress effect. 0 means not calculated
stretch_weight = 5 # change this to increase / decrease the stretch effect. 0 means not calculated

# arrays
bind_points = om.MPointArray()
bind_fn.getPoints(bind_points, om.MSpace.kObject)

bind_triangle_count = om.MIntArray()
bind_triangle_indices = om.MIntArray()
bind_fn.getTriangles(bind_triangle_count, bind_triangle_indices)

bind_normal_array = om.MFloatVectorArray()
bind_fn.getVertexNormals(0, bind_normal_array, om.MSpace.kObject)

# get the bind area array from the bind triangles and bind points
bind_area_array_dict = {}
length = bind_triangle_indices.length()
triangle_index = 0

for count in range(0, length, 3):
 triangle = (bind_triangle_indices[count], bind_triangle_indices[count + 1], bind_triangle_indices[count + 2])
 triangleAB = bind_points[triangle[0]] - bind_points[triangle[1]]
 triangleAC = bind_points[triangle[0]] - bind_points[triangle[2]]
 triangleBC = bind_points[triangle[1]] - bind_points[triangle[2]]
 triangleAB_magnitude = triangleAB.length()
 triangleAC_magnitude = triangleAC.length()
 triangleBC_magnitude = triangleBC.length()
 heron = (triangleAB_magnitude + triangleAC_magnitude + triangleBC_magnitude) / 2
 area = math.sqrt(heron * (heron - triangleAB_magnitude) * (heron - triangleAC_magnitude) * (heron - triangleBC_magnitude))
 bind_area_array_dict[triangle_index] = [triangle, area]
 triangle_index += 1

SCRIPT PART 2 TO BE RUN ON DEFORMED SHAPE

# NOW POSE YOUR MESH AND RUN THIS. If the mesh is bound you will need to unbind it for this part to work. If you decide to build this as a deformer you will not need to address this

sel.getDagPath(0, dag_path)
deformed_fn = om.MFnMesh(dag_path)
 
# get the point positions for the deformed mesh
deformed_points = om.MPointArray()
deformed_fn.getPoints(deformed_points, om.MSpace.kObject )

# get the deformed area array from the bind triangles and deformed points
deformed_area_array_dict = {}
length = bind_triangle_indices.length()
triangle_index = 0

for count in range(0, length, 3):
 triangle = (bind_triangle_indices[count], bind_triangle_indices[count + 1], bind_triangle_indices[count + 2])
 triangleAB = deformed_points[triangle[0]] - deformed_points[triangle[1]]
 triangleAC = deformed_points[triangle[0]] - deformed_points[triangle[2]]
 triangleBC = deformed_points[triangle[1]] - deformed_points[triangle[2]]
 triangleAB_magnitude = triangleAB.length()
 triangleAC_magnitude = triangleAC.length()
 triangleBC_magnitude = triangleBC.length()
 heron = (triangleAB_magnitude + triangleAC_magnitude + triangleBC_magnitude) / 2
 area = math.sqrt(heron * (heron - triangleAB_magnitude) * (heron - triangleAC_magnitude) * (heron - triangleBC_magnitude))
 deformed_area_array_dict[triangle_index] = [triangle, area]
 triangle_index += 1


#get the vertex normals for the deformed mesh
deformed_normal_array = om.MFloatVectorArray()
deformed_fn.getVertexNormals(0, deformed_normal_array, om.MSpace.kObject)

length = len(deformed_area_array_dict)
done_array = []

for num in range(length):

 # check to see if the triangle area between the bind and current is different. If less its compressing, if more its stretching
 deformation_amount = deformed_area_array_dict[num][1] - bind_area_array_dict[num][1]

 if deformation_amount < -0.0001 and compress_weight != 0 or deformation_amount > 0.0001 and stretch_weight != 0:

  compress = False
  stretch = False

  if deformation_amount < -0.0001:
   compress = True

  if deformation_amount > 0.0001:
   stretch = True

  # get list of all indices in current triangle
  idx1 = deformed_area_array_dict[num][0][0]
  idx2 = deformed_area_array_dict[num][0][1]
  idx3 = deformed_area_array_dict[num][0][2]

  # get the current position of each vertex using the indices
  vtx1 = deformed_points[idx1]
  vtx2 = deformed_points[idx2]
  vtx3 = deformed_points[idx3]

  # calculate the delta of the vertices between the bind and the input compress shape
  if compress:
   delta1 = compress_points[idx1] - bind_points[idx1]
   delta2 = compress_points[idx2] - bind_points[idx2]
   delta3 = compress_points[idx3] - bind_points[idx3]

  if stretch:
   delta1 = stretch_points[idx1] - bind_points[idx1]
   delta2 = stretch_points[idx2] - bind_points[idx2]
   delta3 = stretch_points[idx3] - bind_points[idx3]

  # multiply the weights. delta * deformation amount * compress or stretch weight * overall weight
  if compress:
   delta1 *= compress_weight * overall_weight * abs(deformation_amount)
   delta2 *= compress_weight * overall_weight * abs(deformation_amount)
   delta3 *= compress_weight * overall_weight * abs(deformation_amount)

  if stretch:
   delta1 *= stretch_weight * overall_weight * abs(deformation_amount)
   delta2 *= stretch_weight * overall_weight * abs(deformation_amount)
   delta3 *= stretch_weight * overall_weight * abs(deformation_amount)
   
  # get the current normal direction on the deformed shape - object space - and convert to a MVector from MFloatVector
  deformed_nor1 = om.MVector(deformed_normal_array[idx1])
  deformed_nor2 = om.MVector(deformed_normal_array[idx2])
  deformed_nor3 = om.MVector(deformed_normal_array[idx3])

  # get the corresponding normal direction on the bind shape - object space - and convert to a MVector from MFloatVector
  bind_nor1 = om.MVector(bind_normal_array[idx1])
  bind_nor2 = om.MVector(bind_normal_array[idx2])
  bind_nor3 = om.MVector(bind_normal_array[idx3])

  # get the dot product of the delta and the bind .This will give us a scaler based on how far the delta vector is projected along the bind vector
  delta_dot1 = bind_nor1 * delta1
  delta_dot2 = bind_nor2 * delta2
  delta_dot3 = bind_nor3 * delta3

  # multiply the deformed normal vector by the delta dot to scale it accordingly
  deformed_nor1 *= delta_dot1
  deformed_nor2 *= delta_dot2
  deformed_nor3 *= delta_dot3

  # add this value to the current deformed vertex position
  vtx1 = (vtx1.x + deformed_nor1.x, vtx1.y + deformed_nor1.y, vtx1.z + deformed_nor1.z)
  vtx2 = (vtx2.x + deformed_nor2.x, vtx2.y + deformed_nor2.y, vtx2.z + deformed_nor2.z)
  vtx3 = (vtx3.x + deformed_nor3.x, vtx3.y + deformed_nor3.y, vtx3.z + deformed_nor3.z)
  
  # put the result back into the deformed point array at the correct vertex index
  deformed_points.set(idx1, vtx1[0], vtx1[1], vtx1[2], 1)
  deformed_points.set(idx2, vtx2[0], vtx2[1], vtx2[2], 1)
  deformed_points.set(idx3, vtx3[0], vtx3[1], vtx3[2], 1)

# apply the result back the vertices
deformed_fn.setPoints(deformed_points, om.MSpace.kObject)

1 comment:

  1. Look at your blog. It helps me learn more about the deformer, thx

    ReplyDelete