Principe

Avant de commencez, sachez que ce problème est un cas d’école. Il y a mille et une façons de le résoudre, chaque méthode ayant ces avantages et ces inconvénients. :reflechi:

La méthode choisi ici est la plus simple: Pour chaque vertex de la géométrie, on trouve la position "arrondi au cube près" et on génère un cube.

La seule difficulté de cette implémentation est, une fois la position du vertex récupéré, de savoir ou doit être le centre du cube. :seSentCon:

voxel_vray.png

Le 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)

Les explications

sel = OpenMaya.MSelectionList()

Fondamentalement, une MSelectionList est une liste de MObject.

Je n'ai jamais vraiment compris ce que la notion de "sélection" voulait dire avec cet objet car il ne "sélectionne" rien. :bete:

La particularité d'une MSelectionList c'est de pouvoir récupérer le MObject d'un node Maya depuis son nom, ce qu'on fait plus loin.

dagPath = OpenMaya.MDagPath()

C'est toujours assez difficile d'expliquer ce qu'est le DAG de Maya et encore plus un DAG path. :gne:

Pour faire un gros résumé:

  • DAG: C'est la "hiérarchie" (parent/enfant) de Maya.
  • DAG path: C'est le "chemin" d'un node en passant par la hiérarchie (et donc, en ayant toutes ces transformations).

Beaucoup de fonctions de l'API Maya nécessitent un DAG path pour pouvoir fonctionner.

Par exemple, on ne peut récupérer les coordonnées dans l'espace des vertices d'un node de shape si on ne connait pas le chemin par lequel on y arrive. Deux instances ont un seul node de shape mais il s'agit bien de deux DAG path différents et les coordonnes dans l’espace du même vertex auront deux valeurs possibles suivant "par ou on passe".

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

On ajoute l'object Maya "pSphere1" dans la MSelectionList et on récupère son DAG path (le zéro étant l'index dans la liste de sélection).

Nous avons donc "converti" "pSphere1" (qui ne veut rien dire en API Maya) en vrai MObject.

inMesh = OpenMaya.MFnMesh( dagPath )

Par défaut, tout ce qu'on récupère dans l'API ce sont des MObjects. En général, on vérifie le type du MObject via un MObject.apiType() ou MObject.hasFn()

Mais là on part du principe que le user a bien donné une mesh. :siffle:

La fonction MFnMesh permet d'avoir à disposition un "vrai" objet (en terme programmation j'entend) sur lequel on va pouvoir agir pour récupérer des informations. C'est un function set.

Je vous invite a lire la documentation afin de comprendre le comment du pourquoi des function sets.

Nous avons donc un object _inMesh_ qui va nous servir a récupérer les vertices de notre mesh.

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

La première ligne créée un tableau de point MPointArray pour accueillir les positions des vertices de notre mesh.

Et la second ligne remplit le tableau avec les coordonne des vertices en espace monde (position par rapport au centre de la scène, non par rapport au centre de l'objet lui même).

grpName = cmds.group(empty=True)

Ici on ne fait que créer un groupe vide qui accueillera les cubes que nous allons créer par la suite. C'est juste qu'il est plus pratique de supprimer un groupe avec tout dedans que de sélectionner tous les cubes à la pogne. :dentcasse:

voxelIdList = list()

On créé une liste qui servira a stocker les identifiant (ou code) des zones ou les cubes on déjà été générés. J'y reviens plus bas.

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

Et voici la petite ligne qui fait tout. Vous allez voir, elle est très simple.

Alors, qu'est ce qu'on cherche à faire avec ça?

On va partir du principe que la taille voulue des cubes fait 2.0.

On tombe sur un vertex positionné en X à 10.2. Bien entendu, on ne va pas placer notre cube au centre du vertex (à 10.2). On n'obtiendrais pas du tout l'effet voulu. :nannan:

Il nous faut la position exacte du cube. Il faut "comptez en nombre de cube".

Comment savoir à quoi correspond la distance 10.2 en cube de taille 2.0? En faisant tout simplement: 10.2/2.0. Soit 5.1.

Comme on ne peut pas avoir 5.1 cubes, on arrondi via la fonction round(). Dans le cas de round(5.1), on obtient 5 (5.7 aurait donne 6).

On sait donc maintenant que si on crée un cube de taille 2.0, il faut le déplacer de 5 fois sa taille pour qu'il englobe le vertex. On multiplie donc la valeur arrondi (5) par la taille d'un cube (2) pour obtenir une nouvelle position: La position du cube, non plus calée sur le vertex mais calée sur la grille du voxel.

Et voila, vous avez capté! :laClasse:

On fait ça pour les trois axes.

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

Ici, on créé une "signature" (en string) de la position du cube et on la stock dans une liste. Comme ça, si on retombe sur un vertex qui, une fois arrondi, se retrouve au même endroits qu'un cube déjà existant, on ne le crée pas (pas de doublons! :hehe:).

Bien que cette manière de faire semble un peu est très bizarre (convertir les positions des vertex en string), j'ai eu l'impression que c’était la méthode la plus simple pour gérer une grille dont on ne connait pas la taille sans trop de lignes de code.

Mais si vous en avez une autres assez rapide à mettre en place, n’hésitez pas. :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)

Après c'est simple:

  • On créé notre cube de la taille voulu (2.0).
  • On lui applique un bevel parce que ça rend bien. :smileFou:
  • On le parente au groupe.
  • On le place à la position calculée plus tôt.

Et on repart pour un autre vertex!

voxel_maya_api_001.png

voxel_maya_api_002.png

Encore une fois: Ce script n'est pas optimisé du tout, c'est plus un prototype grossier qu'un outil de prod. Il suffit de lui donner un mesh un peu lourd pour s'en rendre compte. :mechantCrash:

Conclusion

Ainsi s’achève ce billet express.

Vous avez vue, c'est assez bête dans le principe. Bon, encore une fois on aurait pu faire différemment et surement plus efficace (essayez avec MFnMesh.allIntersections()).

Personnellement, jouer avec l'API Maya m'amuse toujours autant. :)

A bientôt!

:marioCours:

EDIT 2013/03/17

Je n'ai pas pu résister à l'appel de la méthode MFnMesh.allIntersections(). Voici donc une version bien plus optimisé que précédemment (avec structure d’accélération MFnMesh.autoUniformGridParams()).

La principale différence comparé au code précédent est qu'ici nous ne passons plus par chaque vertex mais nous envoyons des rayon sur trois grille (X, Y, Z).

La seconde différence est que ce code fonctionne sur un mesh animé (1 a 24 ici). Testez, l'effet est assez sympa. :)

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
Désolé pour le code horizontal. :dentcasse:

Je ne fais pas de commentaires supplémentaires car je pense que si vous avez compris le premier code, celui ci est tout aussi simple. :gniarkgniark:

EDIT 2013/03/19

Justin Israel a fait une remarque très pertinante concernant ma voxelIdList. J'ai appris un truc ducoup. Je vous traduis son message ici:

Si tu utilise voxelIdList pour chercher la signature d'un cube déjà placé, le fait que ce soit une liste va progressivement ralentir la recherche au fil qu'elle se remplit, car x in list est de complexité O(n). Tu devrais utiliser un set():

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

Un type set est de complexité O(1) donc faire un x in set va instantanément trouver l'item depuis sa signature, à l'inverse d'une liste qu'une faut parcourir entièrement.