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):
 try:
  print(message)
 except:
  MQSystem.println(str(message))
 return

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
 source: https://sites.google.com/site/glennmurray/Home/rotation-matrices-and-formulas
 '''
  
 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  
  #mqprint(vectors)

  sides = [] # empty list to store lengths of sides
  for v in vectors:
   sides.append(VecLength(v))

  i = 0
  for s in sides:
   if s == max(sides):
    h_index = i    # longest side
   elif s == min(sides):
       s_index = i    # shortest side
   else:
    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
  else:
   root = b.pos

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

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

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

  return result

 else:
  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:
  obj.addVertex(SRTmatrix.mult(v.pos))

 for f in caps_obj.face:
  obj.addFace(list(f.index))
 
 # 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)
  else:
   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
 rot_axis.normalize()
 
 #mqprint(rot_axis)
 
 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)
 
 return

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")
  return
  
 # 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:
   continue
  if ob.name.startswith("bone:"):
   bone_obj = ob
  if ob.name.lower() == "capsule":
   caps_obj = ob
   
 if bone_obj == None:
  mqprint('No "bone:" object found')
  return
  
 if caps_obj == None:
  mqprint('No "capsule" object found')
  return
  
 if bone_obj.numFace == 0:
  mqprint('No faces in "bone:" object')
  return
  
 count = 0
 for f in bone_obj.face:
  if f.numVertex == 3:
   count += 1
 if count == 0:
  mqprint('No triangles in "bone:" object')
  return
 
 if caps_obj.numFace == 0:
  mqprint('No faces in "capsule" object')
  return 
 
 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:
   continue
  data = getData(f, bone_obj)
  if data:
   drawCapsule(influence_object, data["short"], data["long"], data["centre"], data["direction"])
  else:
   mqprint("No data for face %d"%(f.id))
 return

'''
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
 MQSystem.clearLog()

 # 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
 main()

 # 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