VSFX 705 - Programming Concepts for Visual Effects
Particle Cache-based Curve Writers using MEL, RSL, and Python
Project Summary:
Create Python modules that will gather data from a particle system and then use that data to write code to generate curves; either standard Maya NURBS curves or Renderman curves.
Results:
Click on the image to the right to see a sample render. Click here and here to see turntables of curves generated with this code. Click here to see a short animation created using the code. The code sections in their entirety are found in links below.
The Brief
This process requires four integrated operations: gathering the data, parsing it correctly for Maya or Renderman, generating the necessary MEL or RIB file, and outputting the result as curves in Maya or a render. Because of this, we need three separate groups of code. The first gathers the data; the second comes in two versions, each specific to MEL or Rman syntax; and the third writes the generated code to a file to be evaulated by Maya or rendered in Renderman. A fourth small block of code acts as a call to pass arguments to the rest.Below are the main cores of each of the separate sections of code, each block in its entirety with comments can be found in a corresponding link.
The Gathering Code
The code below gathers and holds position data pulled from a particle so that the string-generating sections of code can pull from it. Click to see he full, commented code.
def add( self, xyz ):
self.data.append(xyz)
def addIndiv(self, index, pos):
if len(self.data) > index:
xyz = pos[0:3]
self.data[index].append(xyz[0])
self.data[index].append(xyz[1])
self.data[index].append(xyz[2])
else:
self.data.append(pos[0:3])
def get( self ):
return self.data
def getIndiv(self, index):
return self.data[index]
def setDataPath(self, fullpath):
self.dataPath = fullpath
print("set path = %s" % self.dataPath)
def writeToFile(self):
print("write path = %s" % self.dataPath)
fileid = open(self.dataPath, 'w')
fileid.write(self.dataStr)
fileid.close()
return self.dataPath
self.data.append(xyz)
def addIndiv(self, index, pos):
if len(self.data) > index:
xyz = pos[0:3]
self.data[index].append(xyz[0])
self.data[index].append(xyz[1])
self.data[index].append(xyz[2])
else:
self.data.append(pos[0:3])
def get( self ):
return self.data
def getIndiv(self, index):
return self.data[index]
def setDataPath(self, fullpath):
self.dataPath = fullpath
print("set path = %s" % self.dataPath)
def writeToFile(self):
print("write path = %s" % self.dataPath)
fileid = open(self.dataPath, 'w')
fileid.write(self.dataStr)
fileid.close()
return self.dataPath
The Maya Cache Code
This code is specific to generating the MEL code necessary for Maya to evaluate and create Maya NURBS curves. Click to see the full, commented code. This first section loops thru each frame of the animation and adds positional data for each particle to the storage variable.
def updateCache(self, tnode):
pnum = mc.particle(tnode, q = True, count = True)
for n in range(pnum):
pname = tnode + ".pt[%s]" % n
pos = mc.getParticleAttr(pname,at = 'position')
self.add(pos[0:3])
pnum = mc.particle(tnode, q = True, count = True)
for n in range(pnum):
pname = tnode + ".pt[%s]" % n
pos = mc.getParticleAttr(pname,at = 'position')
self.add(pos[0:3])
This section generates a single curve made of all the positions of each particle over time.
def writeCurve(self):
melcmd = 'curve -d 3 '
count = 1
for xyz in self.data:
melcmd = melcmd + '-p %1.3f %1.3f %1.3f ' %
(xyz[0],xyz[1],xyz[2])
count += 1
if count > 5:
melcmd += '\n\t'
count = 1
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
melcmd = 'curve -d 3 '
count = 1
for xyz in self.data:
melcmd = melcmd + '-p %1.3f %1.3f %1.3f ' %
(xyz[0],xyz[1],xyz[2])
count += 1
if count > 5:
melcmd += '\n\t'
count = 1
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
This section generates numerous curves each made of two points; one at the origin and the other corresponding to a single position.
def writeManyCurves(self):
starter = 'curve -d 1 -p 0 0 0 '
melcmd = starter
for xyz in self.data:
melcmd = melcmd + '-p %1.3f %1.3f %1.3f' %
(xyz[0],xyz[1],xyz[2]) + ';\n'
if xyz <= (self.data):
melcmd = melcmd + starter
else:
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
starter = 'curve -d 1 -p 0 0 0 '
melcmd = starter
for xyz in self.data:
melcmd = melcmd + '-p %1.3f %1.3f %1.3f' %
(xyz[0],xyz[1],xyz[2]) + ';\n'
if xyz <= (self.data):
melcmd = melcmd + starter
else:
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
This section again generates a single curve but adds to its positional data a random value to make it waver over its course.
def writeRandomCurve(self):
melcmd = 'curve -d 3 '
count = 1
for xyz in self.data:
randX = random.uniform (.1, .3)
newX = xyz[0] + randX
randY = random.uniform (.1, .3)
newY = xyz[1] + randY
randZ = random.uniform (.1, .3)
newZ = xyz[2] + randZ
melcmd = melcmd + '-p %1.3f %1.3f %1.3f ' %
(newX,newY,newZ)
count += 1
if count > 5:
melcmd += '\n\t'
count = 1
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
melcmd = 'curve -d 3 '
count = 1
for xyz in self.data:
randX = random.uniform (.1, .3)
newX = xyz[0] + randX
randY = random.uniform (.1, .3)
newY = xyz[1] + randY
randZ = random.uniform (.1, .3)
newZ = xyz[2] + randZ
melcmd = melcmd + '-p %1.3f %1.3f %1.3f ' %
(newX,newY,newZ)
count += 1
if count > 5:
melcmd += '\n\t'
count = 1
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
After experimentation I noticed that the "spike" code would make several overlapping curves that appeared as one. This is because it was drawing a curve for each particle at each frame over time and they traveled linear paths away from the origin. This section of code adds a random value to the positional data, causing the overlapping curves to fan out.
def writeManyRandomCurves(self):
starter = 'curve -d 1 -p 0 0 0 '
melcmd = starter
for xyz in self.data:
randX = random.uniform (.1, .3)
newX = xyz[0] + randX
randY = random.uniform (.1, .3)
newY = xyz[1] + randY
randZ = random.uniform (.1, .3)
newZ = xyz[2] + randZ
melcmd = melcmd + '-p %1.3f %1.3f %1.3f' %
(newX,newY,newZ) + ';\n'
if xyz <= (self.data):
melcmd = melcmd + starter
else:
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
starter = 'curve -d 1 -p 0 0 0 '
melcmd = starter
for xyz in self.data:
randX = random.uniform (.1, .3)
newX = xyz[0] + randX
randY = random.uniform (.1, .3)
newY = xyz[1] + randY
randZ = random.uniform (.1, .3)
newZ = xyz[2] + randZ
melcmd = melcmd + '-p %1.3f %1.3f %1.3f' %
(newX,newY,newZ) + ';\n'
if xyz <= (self.data):
melcmd = melcmd + starter
else:
melcmd = melcmd + ';'
self.dataStr = self.dataStr + melcmd
return melcmd
This final section is the one that calls all the previously set up code to gather, parse, and output the data.
pCache = MayaParticleCache()
projUtils = PU.ProjectUtilities()
def particlesToMayaCurves(tnode, startAt, endFrame, curveType):
pCache.setDataPath(projUtils.getDataDir() + "/" +
projUtils.getSceneName() + ".mel");
for currFrame in range(endFrame):
currFrame += 1;
mc.currentTime(currFrame);
print("frame %s" % currFrame)
if currFrame == 1:
pCache.reset()
if currFrame >= startAt and currFrame <= endFrame:
pCache.updateCache(tnode)
if currFrame == endFrame:
if curveType == 1:
return pCache.writeCurve()
elif curveType == 2:
return pCache.writeManyCurves()
elif curveType == 3:
return pCache.writeRandomCurve()
elif curveType == 4:
return pCache.writeManyRandomCurves()
else:
print "FAIL: Did not recognize curve type"
def writeToFile():
return pCache.writeToFile()
projUtils = PU.ProjectUtilities()
def particlesToMayaCurves(tnode, startAt, endFrame, curveType):
pCache.setDataPath(projUtils.getDataDir() + "/" +
projUtils.getSceneName() + ".mel");
for currFrame in range(endFrame):
currFrame += 1;
mc.currentTime(currFrame);
print("frame %s" % currFrame)
if currFrame == 1:
pCache.reset()
if currFrame >= startAt and currFrame <= endFrame:
pCache.updateCache(tnode)
if currFrame == endFrame:
if curveType == 1:
return pCache.writeCurve()
elif curveType == 2:
return pCache.writeManyCurves()
elif curveType == 3:
return pCache.writeRandomCurve()
elif curveType == 4:
return pCache.writeManyRandomCurves()
else:
print "FAIL: Did not recognize curve type"
def writeToFile():
return pCache.writeToFile()
The Renderman Cache Code
This code is specific to generating the Renderman code necessary to build a RIB file that can then be rendered out. Click to see the full, commented code. This first section loops thru each frame of the animation and adds positional data for each particle to the storage variable.Of particular importance is the addIndiv def which adds data in such a way that each particle's positional data is kept separate from the rest, forming a list of lists. This way we can draw curves that follow a particular particle over time.
def updateCache(self, tnode):
pnum = mc.particle(tnode, q = True, count = True)
for n in range(pnum):
pname = tnode + ".pt[%s]" % n
pos = mc.getParticleAttr(pname,at = 'position')
self.add(pos[0:3])
def updateIndivCache(self, tnode):
pnum = mc.particle(tnode, q = True, count = True)
for n in range(pnum):
pname = tnode + ".pt[%s]" % n
pos = mc.getParticleAttr(pname,at = 'position')
self.addIndiv(n, pos)
pnum = mc.particle(tnode, q = True, count = True)
for n in range(pnum):
pname = tnode + ".pt[%s]" % n
pos = mc.getParticleAttr(pname,at = 'position')
self.add(pos[0:3])
def updateIndivCache(self, tnode):
pnum = mc.particle(tnode, q = True, count = True)
for n in range(pnum):
pname = tnode + ".pt[%s]" % n
pos = mc.getParticleAttr(pname,at = 'position')
self.addIndiv(n, pos)
This section generates a single curve made of all the positions of each particle over time.
def writeCurve(self, isRand):
numCvs = len(self.data)
if numCvs < 4:
self.dataStr = "# FAIL: I only have %d cvs" % numCvs
return ""
ribstr = 'Basis "b-spline" 1 "b-spline" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
ribstr += 'Curves "cubic" [%d] "nonperiodic"\n' % (numCvs)
ribstr += ' "P" [\n'
count = 1
widthcount = 1
for xyz in self.data:
ribstr += '%1.3f %1.3f %1.3f ' % (xyz[0],xyz[1],xyz[2])
count += 1
if count > 5:
ribstr += '\n\t'
count = 1
if isRand == 0:
ribstr += '] "constantwidth" [0.01]\n'
elif isRand == 1:
widths = numCvs - 2
ribstr += ']\n'
ribstr += '"width" ['
for n in range(widths):
randNum = random.uniform (0.01, 0.1)
ribstr += '%1.2f ' % randNum
widthcount += 1
if widthcount > 8:
ribstr +='\n'
widthcount = 1
ribstr += ']\n'
self.dataStr = ribstr
return ribstr
numCvs = len(self.data)
if numCvs < 4:
self.dataStr = "# FAIL: I only have %d cvs" % numCvs
return ""
ribstr = 'Basis "b-spline" 1 "b-spline" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
ribstr += 'Curves "cubic" [%d] "nonperiodic"\n' % (numCvs)
ribstr += ' "P" [\n'
count = 1
widthcount = 1
for xyz in self.data:
ribstr += '%1.3f %1.3f %1.3f ' % (xyz[0],xyz[1],xyz[2])
count += 1
if count > 5:
ribstr += '\n\t'
count = 1
if isRand == 0:
ribstr += '] "constantwidth" [0.01]\n'
elif isRand == 1:
widths = numCvs - 2
ribstr += ']\n'
ribstr += '"width" ['
for n in range(widths):
randNum = random.uniform (0.01, 0.1)
ribstr += '%1.2f ' % randNum
widthcount += 1
if widthcount > 8:
ribstr +='\n'
widthcount = 1
ribstr += ']\n'
self.dataStr = ribstr
return ribstr
This section generates numerous curves each made of two points; one at the origin and the other corresponding to a single position.
def writeCurves(self, isRand):
numCvs = len(self.data)
if numCvs < 2:
self.dataStr = "# FAIL: I only have %d cvs" % numCvs
return ""
ribstr = 'Basis "bezier" 1 "bezier" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
ribstr += 'Curves "linear" [2] "nonperiodic"\n'
ribstr += ' "P" [0 0 0 '
count = 1
for xyz in self.data:
randX = random.uniform (.1, .3)
newX = xyz[0] + randX
randY = random.uniform (.1, .3)
newY = xyz[1] + randY
randZ = random.uniform (.1, .3)
newZ = xyz[2] + randZ
if isRand == 0:
ribstr += '%1.3f %1.3f %1.3f]\n' % (xyz[0],xyz[1],xyz[2])
elif isRand == 1:
ribstr += '%1.3f %1.3f %1.3f]\n' % (newX,newY,newZ)
ribstr += ' "width" [0.01 0]\n'
count += 1
if count <= numCvs:
ribstr += 'Curves "linear" [2] "nonperiodic"\n'
ribstr += ' "P"[0 0 0 '
self.dataStr = ribstr
return ribstr
numCvs = len(self.data)
if numCvs < 2:
self.dataStr = "# FAIL: I only have %d cvs" % numCvs
return ""
ribstr = 'Basis "bezier" 1 "bezier" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
ribstr += 'Curves "linear" [2] "nonperiodic"\n'
ribstr += ' "P" [0 0 0 '
count = 1
for xyz in self.data:
randX = random.uniform (.1, .3)
newX = xyz[0] + randX
randY = random.uniform (.1, .3)
newY = xyz[1] + randY
randZ = random.uniform (.1, .3)
newZ = xyz[2] + randZ
if isRand == 0:
ribstr += '%1.3f %1.3f %1.3f]\n' % (xyz[0],xyz[1],xyz[2])
elif isRand == 1:
ribstr += '%1.3f %1.3f %1.3f]\n' % (newX,newY,newZ)
ribstr += ' "width" [0.01 0]\n'
count += 1
if count <= numCvs:
ribstr += 'Curves "linear" [2] "nonperiodic"\n'
ribstr += ' "P"[0 0 0 '
self.dataStr = ribstr
return ribstr
This section is actually code that I arrived at by accident when I was first experimenting with code to make spikes. The results intriqued me so I kept the code and tweaked it. It produces groups of curves that orbit the origin.
def writeWavyCurves(self, isRand):
numCvs = len(self.data)
totalcycles = numCvs/4
if numCvs < 4:
self.dataStr = "# FAIL: I only have %d cvs" % numCvs
return ""
ribstr = 'Basis "b-spline" 1 "b-spline" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
ribstr += 'Curves "cubic" [4] "nonperiodic"\n'
ribstr += '"P" ['
increment = 1
cycle = 1
for xyz in self.data:
randNum = random.uniform (0.01, 0.1)
ribstr +='%1.3f %1.3f %1.3f ' % (xyz[0],xyz[1],xyz[2])
increment += 1
if increment > 4:
increment = 1
cycle += 1
ribstr += ']\n'
if isRand == 0:
ribstr += ' "constantwidth" [0.01]\n'
elif isRand == 1:
ribstr += ' "width" [%1.2f 0]\n' % randNum
if cycle <= totalcycles:
ribstr += 'Curves "cubic" [4] "nonperiodic"\n'
ribstr += '"P" ['
else:
break
self.dataStr = ribstr
return ribstr
numCvs = len(self.data)
totalcycles = numCvs/4
if numCvs < 4:
self.dataStr = "# FAIL: I only have %d cvs" % numCvs
return ""
ribstr = 'Basis "b-spline" 1 "b-spline" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
ribstr += 'Curves "cubic" [4] "nonperiodic"\n'
ribstr += '"P" ['
increment = 1
cycle = 1
for xyz in self.data:
randNum = random.uniform (0.01, 0.1)
ribstr +='%1.3f %1.3f %1.3f ' % (xyz[0],xyz[1],xyz[2])
increment += 1
if increment > 4:
increment = 1
cycle += 1
ribstr += ']\n'
if isRand == 0:
ribstr += ' "constantwidth" [0.01]\n'
elif isRand == 1:
ribstr += ' "width" [%1.2f 0]\n' % randNum
if cycle <= totalcycles:
ribstr += 'Curves "cubic" [4] "nonperiodic"\n'
ribstr += '"P" ['
else:
break
self.dataStr = ribstr
return ribstr
This section makes a single curve for each particle that traces its path over time, each emanating from the origin. To distinguish it from the spike code and give the curves some character it necessitates adding some sort of turbulence field to the particle system so that they wander over time.
def writeIndivCurves(self, isRand):
numCvs = len(self.data)
ribstr = 'Basis "b-spline" 1 "b-spline" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
for n in range(numCvs):)
xyz = self.getIndiv(n)
if len(xyz)/3 >= 4:
ribstr += 'Curves "cubic" [%d] "nonperiodic" "P" [\n' %
(len(xyz)/3)
for i in range(len(xyz)):
randAdd = random.uniform (.1, .3)
newPos = xyz[i] + randAdd
if isRand == 0:
ribstr += "%s " % (xyz[i])
elif isRand == 1:
ribstr += "%s " % (newPos)
ribstr += '\n] "constantwidth" [0.01] \n'
self.dataStr = ribstr
return ribstr
numCvs = len(self.data)
ribstr = 'Basis "b-spline" 1 "b-spline" 1\n'
ribstr += 'Attribute "dice" "hair" [1]\n'
ribstr += 'Attribute "stochastic" "int sigma" [1]\n'
for n in range(numCvs):)
xyz = self.getIndiv(n)
if len(xyz)/3 >= 4:
ribstr += 'Curves "cubic" [%d] "nonperiodic" "P" [\n' %
(len(xyz)/3)
for i in range(len(xyz)):
randAdd = random.uniform (.1, .3)
newPos = xyz[i] + randAdd
if isRand == 0:
ribstr += "%s " % (xyz[i])
elif isRand == 1:
ribstr += "%s " % (newPos)
ribstr += '\n] "constantwidth" [0.01] \n'
self.dataStr = ribstr
return ribstr
This final section is the one that calls all the previously set up code to gather, parse, and output the data.
pCache = RmanCurveWriter()
projUtils = PU.ProjectUtilities()
def particlesToRmanCurves(tnode, startAt, endFrame,
isRand, curveType):
pCache.setDataPath(projUtils.getArchiveDir() + "/" +
projUtils.getSceneName() + ".rib");
for currFrame in range(endFrame):
currFrame += 1;
mc.currentTime(currFrame);
print("frame %s" % currFrame)
if currFrame == 1:
pCache.reset()
if currFrame >= startAt and currFrame <= endFrame:
if curveType == 1 or curveType == 2 or curveType == 3:
pCache.updateCache(tnode)
elif curveType == 4:
pCache.updateIndivCache(tnode)
else:
print "FAIL: Did not recognize the curveType value"
break
if currFrame == endFrame:
if curveType == 1:
return pCache.writeCurve(isRand)
elif curveType == 2:
return pCache.writeCurves(isRand)
elif curveType == 3:
return pCache.writeWavyCurves(isRand)
elif curveType == 4:
return pCache.writeIndivCurves(isRand)
else:
print "FAIL: Did not recognize the curveType value"
break
def writeToFile():
return pCache.writeToFile()
projUtils = PU.ProjectUtilities()
def particlesToRmanCurves(tnode, startAt, endFrame,
isRand, curveType):
pCache.setDataPath(projUtils.getArchiveDir() + "/" +
projUtils.getSceneName() + ".rib");
for currFrame in range(endFrame):
currFrame += 1;
mc.currentTime(currFrame);
print("frame %s" % currFrame)
if currFrame == 1:
pCache.reset()
if currFrame >= startAt and currFrame <= endFrame:
if curveType == 1 or curveType == 2 or curveType == 3:
pCache.updateCache(tnode)
elif curveType == 4:
pCache.updateIndivCache(tnode)
else:
print "FAIL: Did not recognize the curveType value"
break
if currFrame == endFrame:
if curveType == 1:
return pCache.writeCurve(isRand)
elif curveType == 2:
return pCache.writeCurves(isRand)
elif curveType == 3:
return pCache.writeWavyCurves(isRand)
elif curveType == 4:
return pCache.writeIndivCurves(isRand)
else:
print "FAIL: Did not recognize the curveType value"
break
def writeToFile():
return pCache.writeToFile()
The code actually typed in the Maya script editor to call everything can be found here. The procedures that it in turn calls to pass arguements to pythaon are here.
The Final Product
Click on the images below to see examples of curves generated with the code. This first section is of variations on Maya curves with a standard curve at left, those from particles directed by a turbulence field in the middle, and with the random attribute added at right.This section shows the numerous variations created by combining the four curve types: single, spike, waxy, and follow, with three variations of shader: normal, particle, and sparky, and an amount of randomness.