Monday, 16 June 2014

Metasequoia Script – Mikoto bdef bone influence

See this post [link] for the basics of scripting in Metasequoia.

In this post [link] I talk about how Mikoto assigns vertices to a bone using the geometry of the triangle created in Metasequoia for bdef meshes without using anchors.

To help visualise which vertices will be bound to a bone I wrote a script that shows the zone of influence of each bone.

The Metasequoia Python API doesn’t have routines to create Primitive objects so rather than create an object in code this script requires a Primitive to be created in Metasequoia which it can copy.

The Primitive object must be named capsule and have its properties set as shown in the screenshot below.

Create a capsule primitive with these settings

Note my tabs have been converted to one space indentation in the code below.

Metasequoia Python script to show Mikoto bdef default bone influence
Only tested in version 4.2.2 trial

Before running the script:

 1) an object named with "bone:" as the prefix and containing triangles must exist
 2) an object named "capsule" with a material assigned to all faces must exist
 3) create the "capsule" object using the Primitive in the right hand column and bottom row
 4) the "capsule" object must be vertical
 5) set the properties for the "capsule" object as:

  X = 0, Y = 0, Z = 0

         Segment  Size
    U      20      1
    V       1      4
    R       4      1

 6) the material for the "capsule" object should have an alpha set to 0.5 to make it see through
 7) the "capsule" object may be hidden and/or locked

After the script has finished a new object should have been created containing child objects
for each triangle in the "bone:" object showing the corresponding Mikoto bone's zone of influence 


# import Python's math module so its trigonometry functions and "pi" constant can be used
import math

# import Python's sys module so its "version_info" constant can be used
import sys

# global variables -> all functions can use them
doc = MQSystem.getDocument()
bone_obj = None
caps_obj = None
influence_object = None

# Metasequoia 4 -> can use print() or MQSystem.println() in scripts to print to output window
# Metasequoia 3 -> can only use MQSystem.println() in scripts to print to output window

def mqprint(message):

def ScalarTimesVec(scalar, vec):
 outvec = MQSystem.newPoint(scalar*vec.x, scalar*vec.y, scalar*vec.z)
 return outvec

def VecLength(vec):
 return (vec.x*vec.x+vec.y*vec.y+vec.z*vec.z)**0.5

def RadToDegrees(angle):
 return angle*180.0/math.pi

def RotPtAboutAxis(point, axis_pt, axis, angle):
 axis_pt is point on the axis
 axis must be unit vector
 angle in radians
 x, y, z = point.x, point.y, point.z
 a, b, c = axis_pt.x, axis_pt.y, axis_pt.z
 u, v, w = axis.x, axis.y, axis.z
 theta = angle
 t = 1 - math.cos(theta)
 ct = math.cos(theta)
 st = math.sin(theta)
 x1 = (a*(v*v+w*w)-u*(b*v+c*w-u*x-v*y-w*z))*t+x*ct+(-c*v+b*w-w*y+v*z)*st
 y1 = (b*(u*u+w*w)-v*(a*u+c*w-u*x-v*y-w*z))*t+y*ct+( c*u-a*w+w*x-u*z)*st
 z1 = (c*(u*u+v*v)-w*(a*u+b*v-u*x-v*y-w*z))*t+z*ct+(-b*u+a*v-v*x+u*y)*st
 return MQSystem.newPoint(x1,y1,z1) 

def getCapsuleMat():
 '''This function returns material index used for first face in "capsule" object'''

 return caps_obj.face[0].material

def getData(f, obj):
 '''This function returns length of short side, length of long side, direction
    of long side and middle of long side of a "bone:" object triangle'''

 result = {} # empty dictionary to store results
 if f.numVertex == 3:
  a = obj.vertex[f.index[0]]
  b = obj.vertex[f.index[1]]
  c = obj.vertex[f.index[2]]
  vectors = [] # empty list to store vectors
  vectors.append(b.pos - a.pos) # a -> b
  vectors.append(c.pos - b.pos) # b -> c 
  vectors.append(c.pos - a.pos) # a -> c  

  sides = [] # empty list to store lengths of sides
  for v in vectors:

  i = 0
  for s in sides:
   if s == max(sides):
    h_index = i    # longest side
   elif s == min(sides):
       s_index = i    # shortest side
    l_index = i   # long side
   i += 1 

  # find root vertex opposite longest side
  if h_index == 0:
   root = c.pos
  elif h_index == 1:
   root = a.pos
   root = b.pos

  # find tip vertex opposite shortest side
  if s_index == 0:
   tip = c.pos
  elif s_index == 1:
   tip = a.pos
   tip = b.pos

  direction = tip - root
  centre = root + ScalarTimesVec(0.5, (direction))
  # make direction a unit vector

  result["direction"] = direction
  result["long"] = sides[l_index]
  result["short"] = sides[s_index]
  result["centre"] = centre

  return result

  return result

def drawCapsule(parent_obj, radius, length, centre, direction):
 '''This function creates a copy of the "capsule" object, scales it according to the short side
    of a "bone:" object triangle and aligns it with the long side of a "bone:" object triangle'''
 #mqprint("R %f, L %f, C %s, D %s"%(radius, length, str(centre), str(direction)))
 obj = MQSystem.newObject()
 R_size = 1.0 # "capsule" object R size
 ratio = radius/R_size # scale radius to short side length
 svector = MQSystem.newPoint(ratio,ratio,ratio)
 rvector = MQSystem.newAngle()
 tvector = MQSystem.newPoint()
 SRTmatrix = MQSystem.newMatrix()
 SRTmatrix.setTransform(svector, rvector, tvector)
 # scale the capsule in all axes
 for v in caps_obj.vertex:

 for f in caps_obj.face:
 # move vertices of the capsule in vertical (Y) axis
 for v in obj.vertex:
  if v.pos.y > 0:
   v.pos.y -= (1*ratio - length/2.0)
   v.pos.y += (1*ratio - length/2.0)
 capsule_vec = MQSystem.newPoint(0.0,1.0,0.0) # capsule object axis unit vector
 rot_axis = capsule_vec.crossProduct(direction) # axis perpendicular to the two vectors
 cosTheta = capsule_vec.dotProduct(direction) # cosine of angle between the two unit vectors
 if cosTheta > 1.0:
  cosTheta = 1.0
 if cosTheta < -1.0:
  cosTheta = -1.0
 rot_angle = math.acos(cosTheta) # rot_angle in radians
 #mqprint(RadToDegrees(rot_angle)) # always positive
 axis_pt = MQSystem.newPoint()
 for v in obj.vertex:
  v.setPos(RotPtAboutAxis(v.pos, axis_pt, rot_axis, rot_angle))
 svector = MQSystem.newPoint(1.0, 1.0, 1.0)
 rvector = MQSystem.newAngle()
 tvector = MQSystem.newPoint(centre.x, centre.y, centre.z)
 # translate capsule centre to middle of long side
 SRTmatrix.setTransform(svector, rvector, tvector) 
 for v in obj.vertex:
  v.pos = SRTmatrix.mult(v.pos)  
 # assign capsule material to created object
 mat = getCapsuleMat()
 for f in obj.face:
  f.material = mat
 if not (doc.material[mat].alpha < 1.0):
  doc.material[mat].alpha = 0.5
 # add object to document
 idx = doc.addObject(obj, parent_obj)

def main():
 Write the script as a function so can stop the script by using a "return"
 statement if necessary to exit the function.
 It is not required to name this function "main".
 if doc.numObject == 0:
  mqprint("No objects")
 # make this function use the global variables and not create local variables with the same
 # names because these names appear on the left hand side of an assignment (=) statement
 global caps_obj
 global bone_obj
 global influence_object
 for ob in doc.object:
  if ob == None:
   bone_obj = ob
  if == "capsule":
   caps_obj = ob
 if bone_obj == None:
  mqprint('No "bone:" object found')
 if caps_obj == None:
  mqprint('No "capsule" object found')
 if bone_obj.numFace == 0:
  mqprint('No faces in "bone:" object')
 count = 0
 for f in bone_obj.face:
  if f.numVertex == 3:
   count += 1
 if count == 0:
  mqprint('No triangles in "bone:" object')
 if caps_obj.numFace == 0:
  mqprint('No faces in "capsule" object')
 obj = MQSystem.newObject()
 idx = doc.addObject(obj)
 influence_object = doc.object[idx]
 # make the object the current object
 doc.currentObjectIndex = idx
 for f in bone_obj.face:
  if f.numVertex != 3:
  data = getData(f, bone_obj)
  if data:
   drawCapsule(influence_object, data["short"], data["long"], data["centre"], data["direction"])
   mqprint("No data for face %d"%(

If this script is executed (Run) its __name__ variable is the string "__main__" and the
statements in the following "if" block execute but if this script is imported into
another script its __name__ variable is not "__main__" so the "if" block statements
will not be executed


if __name__ == "__main__":
 # clear the Metasequoia Script Editor output window each time before the script is run

 # print Python major.minor.revision version
 # Metasequoia 3 uses Python 2.*, Metasequoia 4 uses Python 3.*
 mqprint("Python version is %d.%d.%d"%(sys.version_info[0], sys.version_info[1], sys.version_info[2]))

 # call the function "main()" to execute its code

 # let user know script has finished
 mqprint("Script finished")

Below is a screenshot showing the skeleton in Metasequoia before running the script.

Before running the script

Below is a screenshot after running the script showing the zone of influence for the triangles of the skeleton.

After running the script

No comments:

Post a Comment