Voxelize Meshes Script v.2

February 25th, 2010

With the help of the patient people in the python_inside_maya forum, I’ve improved the Voxelize Meshes Script, mostly by more efficient use of the allIntersections method.

Instead of checking each point on the grid to see whether it’s inside one of the target meshes, this version shoots rays through the meshes along each axis and puts blocks at the intersections. This makes it approximately a zillion times faster, though I’m sure it could still be improved.

Update: Richard Kazuo from the p_i_m forum has excised lingering traces of pymel from my script, I’ve updated the code below with his improved version. It should now run with Maya’s default Python installation. Thanks Richard!

Update 2: Here’s the even-more-efficient voxelize_meshes_v.3.py … I’m putting this to bed now.

Maya Python code:

### start: voxelize meshes v.2.5
### this script turns the selected meshes into cubes
### over the current timeline range.

import maya.OpenMaya as om
import maya.cmds as cmds
import maya.mel as mel

# shoot a ray from point in direction and return all hits with mesh
def rayIntersect(mesh, point, direction=(0.0, 0.0, -1.0)):
  # get dag path of mesh - so obnoxious
  om.MGlobal.clearSelectionList()
  om.MGlobal.selectByName(mesh)
  sList = om.MSelectionList()
  om.MGlobal.getActiveSelectionList(sList)
  item = om.MDagPath()
  sList.getDagPath(0, item)
  item.extendToShape()
  fnMesh = om.MFnMesh(item)

  raySource = om.MFloatPoint(point[0], point[1], point[2], 1.0)
  rayDir = om.MFloatVector(direction[0], direction[1], direction[2])
  faceIds            = None
  triIds             = None
  idsSorted          = False
  worldSpace         = om.MSpace.kWorld
  maxParam           = 99999999
  testBothDirections = False
  accelParams        = None
  sortHits           = True
  hitPoints          = om.MFloatPointArray()
  hitRayParams       = om.MFloatArray()
  hitFaces           = om.MIntArray()
  hitTris            = None
  hitBarys1          = None
  hitBarys2          = None
  tolerance          = 0.0001

  hit = fnMesh.allIntersections(raySource, rayDir, faceIds, triIds, idsSorted, worldSpace, maxParam, testBothDirections, accelParams, sortHits, hitPoints, hitRayParams, hitFaces, hitTris, hitBarys1, hitBarys2, tolerance)

  # clear selection as may cause problems if called repeatedly
  om.MGlobal.clearSelectionList()
  result = []
  for x in range(hitPoints.length()):
    result.append((hitPoints[x][0], hitPoints[x][1], hitPoints[x][2]))
  return result

# round to nearest fraction in decimal form: 1, .5, .25
def roundToFraction(input, fraction):
  factor = 1/fraction
  return round(input*factor)/factor

# progress bar, enabling "Esc"
def makeProgBar(length):
  global gMainProgressBar
  gMainProgressBar = mel.eval('$tmp = $gMainProgressBar');
  cmds.progressBar( gMainProgressBar,
        edit=True,
        beginProgress=True,
        isInterruptable=True,
        maxValue=length
        )

def promptNumber():
  result = cmds.promptDialog(
      title='Grow Shrub',
      message='Block size:',
      text="1",
      button=['OK', 'Cancel'],
      defaultButton='OK',
      cancelButton='Cancel',
      dismissString='Cancel')
  if result == 'OK':
    return float(cmds.promptDialog(query=True, text=True))
  else: return 0

def init(cubeSize):
  global cubeDict, xLocs, yLocs, zLocs
  cubeDict = {}
  xLocs = []
  yLocs = []
  zLocs = []

  # make 3 arrays of ray start points, one for each axis
  # this is necessary because rays aren't likely to catch surfaces
  # which are edge-on... so to make sure to catch all faces,
  # we shoot rays along each axis
  fac = 1/cubeSize
  for y in range(ymin*fac, ymax*fac+1):
    for z in range(zmin*fac, zmax*fac+1):
      loc = (xmax, y*cubeSize, z*cubeSize)
      xLocs.append(loc)
  for z in range(zmin*fac, zmax*fac+1):
    for x in range(xmin*fac, xmax*fac+1):
      loc = (x*cubeSize, ymax, z*cubeSize)
      yLocs.append(loc)
  for x in range(xmin*fac, xmax*fac+1):
    for y in range(ymin*fac, ymax*fac+1):
      loc = (x*cubeSize, y*cubeSize, zmax)
      zLocs.append(loc)

# start the action
if len(cmds.ls(sl=1)) == 0:
  result = cmds.confirmDialog( title='Mesh selection', message= 'Please select a mesh.', button=['OK'])
else:
  startTime= cmds.timerX()
  cubeSize = promptNumber()

  # set selected objects to be the shape targets, aka controls
  ctrl = cmds.ls(sl=1)

  firstFrame = int(cmds.playbackOptions(query=1, min=1))
  lastFrame = int(cmds.playbackOptions(query=1, max=1))
  duration = int(lastFrame-firstFrame)

  makeProgBar(duration*len(ctrl))
  cmds.progressBar(gMainProgressBar, edit=True, beginProgress=1)

  bb = cmds.exactWorldBoundingBox(ctrl[0])
  xmin = bb[0]
  xmax = bb[3]
  xdist = abs(xmin)+abs(xmax)
  ymin = bb[1]
  ymax = bb[4]
  ydist = abs(ymin)+abs(ymax)
  zmin = bb[2]
  zmax = bb[5]

  print "Finding bounding box of animation..."
  print "Press ESC to cancel"
  # find outer boundaries of animation
  for f in range(firstFrame,lastFrame):
    for c in ctrl:
      if cmds.progressBar(gMainProgressBar, query=1, isCancelled=1 ):
        break
      cmds.currentTime(f)
      cmds.progressBar(gMainProgressBar, edit=1, step=1)

      bb = cmds.exactWorldBoundingBox(c)
      xmin = min(xmin, bb[0])
      xmax = max(xmax, bb[3])
      ymin = min(ymin, bb[1])
      ymax = max(ymax, bb[4])
      zmin = min(zmin, bb[2])
      zmax = max(zmax, bb[5])

  cmds.progressBar(gMainProgressBar, edit=1, endProgress=1)

  init(cubeSize)

  cmds.progressBar(gMainProgressBar, edit=1, endProgress=1)
  makeProgBar(duration*len(ctrl))
  cmds.progressBar(gMainProgressBar, edit=1, beginProgress=1)

  print "Animating visibility over", duration, "frames..."
  print "Press ESC to cancel"

  # animate cube visibility
  resetList = []
  for f in range(firstFrame,lastFrame+1): # for each frame
    cmds.currentTime(f, edit=1, update=1)
    # if the cube was visible last frame, hide it
    for x in resetList:
      cmds.setKeyframe(x, at="scale", v=0, t=f)
    resetList = []
    locArrays = [xLocs, yLocs, zLocs]
    directions = [(-1.0, 0.0, 0,0), (0.0, -1.0, 0,0), (0.0, 0.0, -1.0)]
    # for every target control object:
    for c in ctrl:
      if cmds.progressBar(gMainProgressBar, query=1, isCancelled=1 ):
        break
      cmds.progressBar(gMainProgressBar, edit=1, step=1)
      # for each axis:
      for i in range(3):
        cmds.flushUndo()
        # for every gridpoint orthagonal to the animation:
        for loc in locArrays[i]:
          hits = []
          # zap a ray thrugh the object
          hits = rayIntersect(c, loc, directions[i])
          for x in hits:
            # snap hit locations to cubegrid
            x = (roundToFraction(x[0], cubeSize), roundToFraction(x[1], cubeSize), roundToFraction(x[2], cubeSize) )
            # if location isn't in cubeDict, add it and a new cube
            if x not in cubeDict:
              cubeDict[x] = cmds.polyCube(sz=1, sy=1, sx=1, cuv=4, d=cubeSize, h=cubeSize, w=cubeSize, ch=1)[0]
              # move cube to quantized location
              cmds.xform(cubeDict[x], t=x)
            # set a scale key
            cmds.setKeyframe(cubeDict[x], at="scale", v=1, t=f)
            # if previous frame didn't have a scale key, set it to 0
            tempCurTime = cmds.currentTime(q=1)-1
            lastKey = cmds.keyframe(cubeDict[x], at="scale", q=1, t=(tempCurTime,tempCurTime), valueChange=1)
            if lastKey == None or lastKey[0] != 1.0:
              cmds.setKeyframe(cubeDict[x], at="scale", v=0, t=(f-1))
            # add cube to resetList
            resetList.append(cubeDict[x])
    cmds.currentTime(f, edit=1, update=1)

  cmds.progressBar(gMainProgressBar, edit=1, endProgress=1)
  totalTime = cmds.timerX(startTime=startTime)
  print("Total Time: "+str(totalTime))

### end voxelize meshes v.2.5
« previously: Voxelize Meshes Script | Home | next: Patrick Jean – Pixels »