Semi-polite Python worm

April 17th, 2009

I was doing so well, placidly retracing my steps through my MEL code and converting it to Python, like a responsible citizen. But then I got impatient, so I skipped straight to the last version of my worm-building code, and Pythonized it all at once using my still-rudimentary Pymel knowledge.

It took a bit longer than I expected. I ran into some funny roadblocks, some because Pymel is still evolving, and some because my brain is. Anyway, it seems to be working, with some small improvements, and only one teensy drawback.

The Python is easier to read and write than the MEL, but it takes eight times longer to run: for 100 pyramids, over 10 runs, an average of 97 seconds versus 12 seconds for the MEL. Whether this is a dirty secret of Pymel or my own ineptitude will take more eptitude to discover.

I may do a side-by-side translation/comparison in the future; there’s a lot of MEL floating around, and some kind of Rosetta stone may be useful. For now, you get the Python.

Code follows:

# start: semi-polite python worm

from pymel import *
import pymel.core.datatypes as dt
import random

startTime=timerX()

# find the center of a face - aka the "centroid"
def centerOfFace(face):
  # get locations of all the face's vertices
  pos = face.getPoints(space="world")
  returnVec = [0, 0, 0]
  # add them all together
  for vec in pos:
    returnVec += vec
  # divide by # of vectors for average
  returnVec /= len(pos)
  return returnVec

# distange between two vectors
def distanceBetween(a, b):
  a = dt.Vector(a)
  b = dt.Vector(b)
  return length(a-b)

def getNormal(someFace):
  fc = centerOfFace(someFace)
  # returns normal relative to face at origin
  norm = someFace.getNormal(space='world')
  returnVec = [fc[0]+norm[0], fc[1]+norm[1], fc[2]+norm[2]]
  return returnVec

# detect sidedness of a point relative to a face
def isInFront(point, face):
  faceNorm = getNormal(face)
  facePos = centerOfFace(face)
  if (getVectorAngle(point, faceNorm, facePos) <= 90):
    return 1
  else:
    return 0

# get angle between two vectors
def getVectorAngle(aLoc, bLoc, orig):
  aLoc = dt.Vector(aLoc)
  bLoc = dt.Vector(bLoc)
  orig = dt.Vector(orig)
  cLoc = aLoc-bLoc
  cLocFloor = round(cLoc, 4)
  if (cLocFloor[0]==0 and cLocFloor[1]==0 and cLocFloor[2]==0):
    return 0
  else:
    # normalize locations to the face center
    aNorm = aLoc-orig
    bNorm = bLoc-orig
    returnAngle = degrees(angle(aNorm, bNorm))
    # round answer down to two decimal places
    returnAngle = round(returnAngle, 2)
    return returnAngle

# get angle between two vertices with respect to a third point
def getVertexAngle(aVert, bVert, orig):
  aVec = dt.Vector(pointPosition(aVert))
  bVec = dt.Vector(pointPosition(bVert))
  return getVectorAngle(aVec, bVec, orig)

# find the centroid of a cone:
# 1/4 of the way from the base to the peak
def centerOfCone(cone):
  coneLoc = cone.t.get()
  coneLoc = datatypes.Vector(coneLoc)
  topLoc = pointPosition(cone.vtx[3]) # 3 = peak
  topLoc = datatypes.Vector(topLoc)
  center = ((topLoc-coneLoc)/4)+coneLoc
  return center

# find nearby cones
def checkProximity(conePos):
  ids = []
  global coneList
  for i in coneList:
    iPos = centerOfCone(i)
    iDist = distanceBetween(iPos, conePos)
    # detection radius: 2r = 2
    if (iDist < 0.70710678118654752440084436210485):
      # 1/2*sqrt(2) : definite intersection
      return "fail"
    elif (iDist < 2.1213203435596425732025330863145):
      # 2/3*sqrt(2) : possible intersection, add to list of suspects
      ids.append(i)
  return ids

def alignPyramids(pyrA, pyrB, fc):
  # pick a vertex on each pyramid
  newVertex = pyrA+".vtx[0]" # 0 is on the base
  oldVertex = pyrB+".vtx[3]" # 3 is the tip
  # get angle between two vertices
  angle = getVertexAngle(newVertex, oldVertex, fc)
  # if not already aligned
  if (angle > 0):
    # rotate new to align the corners
    xform(pyrA, r=1, os=1, ro=[0, angle, 0])
    # check the result
    angle2 = getVertexAngle(newVertex, oldVertex, fc)
    # if still not aligned, go the other way twice
    if (angle2 > 0):
      angle3 = angle * -2
      xform(pyrA, r=1, os=1, ro=[0, angle3, 0])

def copyCone(old):
  # make a list of faces on old that aren't the base
  faces = [1, 2, 3]
  new = ''
  while (len(faces) > 0):
    # pick a random face
    r = random.choice(faces)
    # strike face off the list
    faces.remove(r)
    selectedFace = old.f[r]
    fc = centerOfFace(selectedFace)
    # copy cone and move to selected face
    dupe = duplicate(old)
    new = dupe[0]
    xform(new, a=1, t=[fc[0], fc[1], fc[2]])
    # aim new at face with a temporary normal constraint
    tmpConst = normalConstraint(selectedFace, new, aim=(0, 1, 0))
    delete(tmpConst)
    alignPyramids(new, old, fc)

    # collision detection - is area in new occupied?
    # reference coneList
    global coneList
    # check for other cones in search radius
    conePos = centerOfCone(new)
    suspects = checkProximity(conePos)
    if (suspects == "fail"):
      delete(new) # start over
      new = "fail"
    else:
      for i in suspects:
        if (pyramidsIntersect(new, i) == 1):
          # intersection found, try another face
          delete(new)
          new = "fail"
          break
      # no intersections? copy is good, clear facelist
      if (new == 'fail'):
        continue
      else:
        faces = []
  # facelist empty?
  return new

# do pyramids intersect?
def pyramidsIntersect(pyrA, pyrB):
  # get all vertexes in pyrA
  pos = pyrA.getPoints(space="world")
  for i in pos:
    # for each point in pyrA
    if isInPyramid(i, pyrB):
      return 1

  # not yet? try the other way around
  pos = pyrB.getPoints(space="world")
  for i in pos:
    if isInPyramid(i, pyrA):
      return 1
  # made it this far? then fail
  return 0

def isInPyramid(point, pyramid):
  for face in pyramid.faces:
    if (isInFront(point, face)):
      return 0 # it's outside
  # if it's behind all four faces, it's inside
  return 1

# initialize global variable
coneList = []

def doit():
  # make a tetrahedron
  cone = polyCone(r=1, h=1.414214, sx=3)[0]
  # move its pivot point to the bottom face
  xform(rp=(0, -0.707107, 0))
  # move to origin and freeze xforms
  xform(t=(0, 0.707107, 0))
  makeIdentity(apply=True, t=1)

  # initialize cone list
  global coneList
  coneList = [cone]

  totalCones = 100
  growCone = 0 # index of cone to grow from

  # main creation loop
  for i in range(totalCones):
    while (1 == 1):
      # attempt to grow a new cone
      new = copyCone(coneList[growCone])
      # if no growth spaces open on that cone
      if (new == "fail"):
        # remove it from the coneList
        del coneList[growCone]
        # pick another random cone in the list and try again
        growCone = random.randint(0,len(coneList)-1)
      else: # success!
        setKeyframe(new, attribute = "visibility", v = 0, t = 1)
        setKeyframe(new, attribute = "visibility", v = 1, t = i+2)
        break

    # if success, add new cone to list
    coneList.append(new)
    growCone = len(coneList)-1

  # delete all bottom faces to lighten the load
  for i in coneList:
    delete(i.f[0])

  del coneList

doit()
select(None)
print("done!")
totalTime = timerX(startTime=startTime)
print("Total Time: "+str(totalTime))

# end
« previously: Taste of Surimi | Home | next: Polite shrub »

Leave a Reply