Principe

Before we begin, know that this problem is a textbook case. There are a thousands of ways to solve it and each method has these pro and con. :reflechi:

The method chosen here is simple: For each vertex of the geometry, we find the position "in cube unit" and generates a cube.

The only dificulty with this implementation is, once the vertex position recovered, to know where must be the center of the cube. :seSentCon:

voxel_vray.png

The code

import maya.cmds as cmds
import maya.OpenMaya as OpenMaya
 
voxelStep = 1.0
 
sel = OpenMaya.MSelectionList()
dagPath = OpenMaya.MDagPath()
 
sel.add("pSphere1")
sel.getDagPath(0, dagPath)
 
inMesh = OpenMaya.MFnMesh( dagPath )
pointArray = OpenMaya.MPointArray()
inMesh.getPoints(pointArray, OpenMaya.MSpace.kWorld)
 
grpName = cmds.group(empty=True)
 
voxelIdList = list()
 
for i in xrange(pointArray.length()) :
 
	cubePosX = round(pointArray[i].x/voxelStep)*voxelStep
	cubePosY = round(pointArray[i].y/voxelStep)*voxelStep
	cubePosZ = round(pointArray[i].z/voxelStep)*voxelStep
 
	cubeId = "%s%s%s" % (cubePosX, cubePosY, cubePosZ)
 
	if cubeId in voxelIdList :
		continue
	else :
		voxelIdList.append(cubeId)
 
	myCube = cmds.polyCube(width=voxelStep, height=voxelStep,  depth=voxelStep)[0]
	cmds.polyBevel(myCube, offset=0.02)
	cmds.parent(myCube, grpName)
	cmds.setAttr(myCube+".translate", cubePosX, cubePosY, cubePosZ)

Explainations

sel = OpenMaya.MSelectionList()

Basically, a MSelectionList is a list of MObject.

I never really understood what the term "selection" meant in this context as it actually does not "select" anything. :bete:

The particularity of a MSelectionList is to retrieve the MObject of a Maya node from its name, what we will do later.

dagPath = OpenMaya.MDagPath()

It is always difficult to explain to begginers what is a DAG and a DAG path. :gne:

As a large summary:

  • DAG: This is the Maya "hierarchy" (parent/child).
  • DAG path: This is the "path" of a node through the hierarchy (and thus, having all these transformations).

Many Maya API functions require DAG path to work.

For example, you can't retrieve world space coordinates of shape node's vertices if you don't know the path by which you get there. Two instances have only one shape node but it is indeed two different DAG path and world space coordinates of one vertex can have two possible values depending "from where we go".

sel.add("pSphere1")
sel.getDagPath(0, dagPath)

We add the Maya object "pSphere1" in the MSelectionList and we retrieve its DAG path (zero is the index in the list).

So we've "converted" "pSphere1" (that doesn't mean anything in Maya API) in true MObject.

inMesh = OpenMaya.MFnMesh( dagPath )

By default, all what you get from the API are MObjects. In general, we check the MObject type using MObject.apiType() or MObject.hasFn().

But here we assume the user provide a mesh. :siffle:

The MFnMesh class allow you to deal with a "true" programming object from which we will be able to get informations. It's a function set.

I invite you to read the documentation to understand the how and why of function sets.

So we have a _inMesh_ that we will use to retrieve mesh's vertices.

pointArray = OpenMaya.MPointArray()
inMesh.getPoints(pointArray, OpenMaya.MSpace.kWorld)

The first line creates a MPointArray to store our mesh vertices positions.

And the second line fills the array with vertices coordinates world space (position relative to the center of the scene, not the center of the object itself).

grpName = cmds.group(empty=True)

Here we only create an empty group that will store cubes we will create later. It is just more convenient to delete a group with everything in it than select all the cubes manually. :dentcasse:

voxelIdList = list()

We create a list which will be used to store identifiers (or keys) of areas where cubes have already been generated. I return below.

cubePosX = round(pointArray[i].x/voxelStep)*voxelStep
cubePosY = round(pointArray[i].y/voxelStep)*voxelStep
cubePosZ = round(pointArray[i].z/voxelStep)*voxelStep

And here are the small lines that does everything. You'll see, it is very simple.

So, what are we trying to do with this?

Let's suppose the desired size cubes is 2.0.

It comes across a vertex set with a X value of 10.2. Of course, we will not put our cube in the center of the vertex (10.2). We would not get the desired effect at all. :nannan:

We need the exact position of the cube. We must "count the number of cube."

How do I know what is the 10.2 distance in cube size 2.0? By simply: 10.2/2.0 = 5.1.

As we cann't have 5.1 cubes, we round using the round() function. In the case of round(5.1), we have 5 (5.7 would give 6).

So now we know that if we create a cube of size 2.0, you should move it 5 times its size to make it emcompass the vertex. We then multiply the rounded value (5) by the size of a cube (2) to obtain a new position: The position of the cube, not wedged on the vertex but keyed to the voxel grid.

And voila! Now you know! :laClasse:

We did this for the three axes.

cubeId = "%s%s%s" % (cubePosX, cubePosY, cubePosZ)
 
if cubeId in voxelIdList :
	continue
else :
	voxelIdList.append(cubeId)

Here, we create a "hash" (a string) of the cube position and we store it in a list. That way, if we fall on a vertex which, once rounded, is in the same places than an already existing cube, it does not create it (no duplicates! :hehe:).

Although method seems a little very odd (convert vertex positions to string), I felt it was the easiest way to manage a unknown sized grid without too many lines of code.

But if you have another one short and quick to implement, don't hesitate to share. : D

myCube = cmds.polyCube(width=voxelStep, height=voxelStep,  depth=voxelStep)[0]
cmds.polyBevel(myCube, offset=0.02)
cmds.parent(myCube, grpName)
cmds.setAttr(myCube+".translate", cubePosX, cubePosY, cubePosZ)

After all of this, everything is simple:

  • We create our cube from it desired size (2.0).
  • We applied a bevel on it because it looks good. :smileFou:
  • We parent it to the group.
  • We place it to the calculated position.

And start again for another vertex!

voxel_maya_api_001.png

voxel_maya_api_002.png

Again: This script is not optimized at all, it is a rough prototype for training purpose, not a production tool. Just give it a heavy mesh to realize. :mechantCrash:

Conclusion

This express ticket is over.

As you see, principle is quite simple. Well, once again we could have done this differently and probably more effective (try with MFnMesh.allIntersections).

Personally, playing with the Maya API always amuses me so much. :)

See you!

:marioCours:

EDIT 2013/03/17

I couldn't resist to try MFnMesh.allIntersections(). There is a well more optimized version (with acceleration structure MFnMesh.autoUniformGridParams()).

The main difference between the previous one and this one is we don't go through each vertex anymore but we project rays through a grid instead (X, Y, Z).

The second difference is the code work on a animated mesh (1 to 24 here). You should test, the effect is cool. :)

import maya.cmds as cmds
import maya.OpenMaya as OpenMaya
 
startFrame = 1
endFrame = 24
 
voxelSize = 20.0
voxelStep = 0.5
 
sel = OpenMaya.MSelectionList()
dagPath = OpenMaya.MDagPath()
 
sel.add("pSphere1")
sel.getDagPath(0, dagPath)
 
inMesh = OpenMaya.MFnMesh( dagPath )
 
grpReelNames = dict()
for curTime in xrange(startFrame, endFrame+1) :
	grpName = "frameGrp_%s".zfill(4) % int(curTime)
	grpReelName = cmds.group(name=grpName, empty=True)
	cmds.setKeyframe(grpReelName+".visibility", value=0.0, time=[curTime-0.1])
	cmds.setKeyframe(grpReelName+".visibility", value=1.0, time=[curTime])
	cmds.setKeyframe(grpReelName+".visibility", value=0.0, time=[curTime+1])
	grpReelNames[curTime] = grpReelName
 
for grpReelName in grpReelNames :
	if cmds.objExists(grpReelName) :
		cmds.delete(grpReelName)
 
for curTime in xrange(startFrame, endFrame+1) :
 
	cmds.currentTime(curTime)
 
	voxelIdList = list()
 
	#I use while just because xrange with floats is impossible
	i = -voxelSize/2.0
	while i <= voxelSize/2.0 :
 
		j = -voxelSize/2.0
		while j <= voxelSize/2.0 :
			for axis in ["zSide", "ySide", "xSide"] :
				z = 0
				y = 0
				x = 0
				zOffset = 0
				zDir = 0
				yOffset = 0
				yDir = 0
				xOffset = 0
				xDir = 0
				if axis == "zSide" :
					x = i
					y = j
					zOffset = 10000
					zDir = -1
				elif axis == "ySide" :
					x = i
					z = j
					yOffset = 10000
					yDir = -1
				elif axis == "xSide" :
					y = i
					z = j
					xOffset = 10000
					xDir = -1
 
				raySource = OpenMaya.MFloatPoint( x+xOffset, y+yOffset, z+zOffset )
				rayDirection = OpenMaya.MFloatVector(xDir, yDir, zDir)
				faceIds=None
				triIds=None
				idsSorted=False
				space=OpenMaya.MSpace.kWorld
				maxParam=99999999
				testBothDirections=False
				accelParams=inMesh.autoUniformGridParams()
				sortHits=False
				hitPoints = OpenMaya.MFloatPointArray()
				hitRayParam=None
				hitFacePtr = None#OpenMaya.MScriptUtil().asIntPtr()
				hitTriangle=None
				hitBary1=None
				hitBary2=None
 
				hit = inMesh.allIntersections(raySource,
								rayDirection,
								faceIds,
								triIds,
								idsSorted,
								space,
								maxParam,
								testBothDirections,
								accelParams,
								sortHits,
								hitPoints,
								hitRayParam,
								hitFacePtr,
								hitTriangle,
								hitBary1,
								hitBary2)
				if not hit :
					continue
 
				# for each interestected points
				for k in xrange(hitPoints.length()) :
 
					cubePosX = round(hitPoints[k].x/voxelStep)*voxelStep
					cubePosY = round(hitPoints[k].y/voxelStep)*voxelStep
					cubePosZ = round(hitPoints[k].z/voxelStep)*voxelStep
 
					cubeId = "%s%s%s" % (cubePosX, cubePosY, cubePosZ)
 
					if cubeId in voxelIdList :
						continue
					else :
						voxelIdList.append(cubeId)
 
					myCube = cmds.polyCube(width=voxelStep, height=voxelStep,  depth=voxelStep)[0]
					cmds.polyBevel(myCube, offset=0.02)
					cmds.parent(myCube, grpReelNames[curTime])
					cmds.setAttr(myCube+".translate", cubePosX, cubePosY, cubePosZ)
			j += voxelStep
		i += voxelStep
Sorry for the horizontal code. :dentcasse:

I don't explain this version because I think if you understood the previous one, you shouldn't have too much problems with this one. :gniarkgniark:

EDIT 2013/03/19

Justin Israel caught my attention about my voxelIdList. I've learn something so I share his message with you:

If you are using it as a lookup for the hash of previously seen items, using a list is going to be progressively slower and slower over time as the list grows, because doing "x in list" is O(n) complexity. You might want to use a set():

voxelIdSet = set()
...
if cubeId in voxelIdSet :
    continue
else:
    voxelIdSet.add(cubeId)

A set is O(1) complexity, so doing "x in set" will instantly find the item by its hash, as opposed to have to scan the entire list looking for an equality match.