Sample render

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

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

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

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

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

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

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

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)

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

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

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

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

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

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.

Example curve 1 Example curve 2 Example curve 3

Example curve 4 Example curve 5 Example curve 6

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.

Example render 1 Example render 2 Example render 3

Example render 4 Example render 5 Example render 6

Example render 7 Example render 8 Example render 9

Example render 10 Example render 11 Example render 12

Example render 13 Example render 14 Example render 15