# # Python scripts for reading and writing maya PDC (Particle Disk Cache) files. Something I've always wanted to be able to do, # and once I got it sorted I thought I'd share. # # Example usage: # - Install numpy (http://numpy.scipy.org/) # - Create a maya scene with a particle system (can be 1 particle) # - Cache the system with solvers -> create particle disk cache # - Set the globals below to your cache folder and particle system name # - Run this script from the command line to populate the particle disk cache folder with new sauce # - Wiggle the maya time slider to retrieve your updated data # # Many thanks to Paul Dubois for helping me sort the binary I/O side of this, # and for providing some excellent pointers on how to pythonize this code! # # Author: Drew Skillman # Email: drew@drewskillman.com # import os import sys import dircache import binfile import numpy as np import math import time #from PIL import Image CACHEFOLDER = 'B:/Art/Visualization/pdc/particles' SCENENAME = 'pdc_test' PARTICLESHAPENODE = 'particleShape1' CACHEPATHANDPREFIX = CACHEFOLDER + '/' + SCENENAME + '/' + PARTICLESHAPENODE DOUBLEARRAY = 3 VECTORARRAY = 5 PDC_ATTRS_ALL = { 'lifespanPP' : DOUBLEARRAY, 'finalLifespanPP' : DOUBLEARRAY, 'emitterId' : DOUBLEARRAY, 'age' : DOUBLEARRAY, 'birthTime' : DOUBLEARRAY, 'particleId' : DOUBLEARRAY, 'mass' : VECTORARRAY, 'lastWorldVelocity' : VECTORARRAY, 'worldVelocityInObjectSpace' : VECTORARRAY, 'worldVelocity' : VECTORARRAY, 'lastWorldPosition' : VECTORARRAY, 'worldPosition' : VECTORARRAY, 'acceleration' : VECTORARRAY, 'lastVelocity' : VECTORARRAY, 'velocity' : VECTORARRAY, 'lastPosition' : VECTORARRAY, 'position' : VECTORARRAY, 'rgbPP' : VECTORARRAY, } PDC_ATTRS = { 'position' : VECTORARRAY, 'rgbPP' : VECTORARRAY, 'opacityPP' : DOUBLEARRAY, 'spriteScaleXPP' : DOUBLEARRAY, 'spriteScaleYPP' : DOUBLEARRAY, } def print_timing(func): def wrapper(*arg): t1 = time.time() res = func(*arg) t2 = time.time() print '%s took %0.3f s' % (func.func_name, (t2-t1)) return res return wrapper def collectPDCs(mayaCachePath): ''' collect all the pdc files in a dir ''' listFiles = dircache.listdir(mayaCachePath) listFiles.sort() nFiles = len (listFiles ) newListFile = [] startFile = None # PLD: use None instead of '' for fn in sorted(os.listdir(mayaCachePath)): (shortname, extension) = os.path.splitext(fn) (basename , padFile) = shortname.split(".") newListFile.append(int(padFile)) if startFile is None: startFile = int(padFile) else: startFile = min(startFile, int(padFile)) newListFile.sort() stepFile = newListFile[1] - newListFile[0] return (basename, stepFile, startFile, nFiles) # --------------------------------------------- # The Particle Data Class # --------------------------------------------- class ParticleData(object): def __init__(self, fileName): self.fileName = fileName self.endianness = 1 self.version = 1 self.cParticles = 0 self.cAttributes = 0 self.attribs = [] def read(self): fileHandle = file( self.fileName, "rb" ) binf = reader(fileHandle) binf.byteorder = '>' cookie = binf.streamCookyNoReverse() #print 'cookie:', cookie assert cookie == "PDC ", "Not a PDC file!" self.version, self.endianness = binf.stream("ii") #print 'version ->', self.version #print 'endianness ->', self.endianness unknown = binf.stream("ii") #print 'unknown ->', unknown self.cParticles, self.cAttributes = binf.stream("ii") #print 'cparticles ->', self.cParticles #print 'cattrs ->', self.cAttributes self.attribs = [] for i in range(self.cAttributes): a = Attribute(self.cParticles) a.read(binf) self.attribs.append(a) for a in self.attribs: setattr(self, a.name, a) fileHandle.close() def write(self): ''' dump it back out ''' fileHandle = file( self.fileName, "wb" ) binf = writer(fileHandle) binf.byteorder = '>' binf.streamCookyNoReverse("PDC ") binf.stream('ii', (self.version, self.endianness)) binf.stream('ii', (0,0)) binf.stream('ii', (self.cParticles, self.cAttributes)) for a in self.attribs: print 'writing:', a.name, a a.write(binf) fileHandle.close() class Attribute(object): def __init__(self, cParticles=0): self.cParticles = cParticles self.name = '' self.type = DOUBLEARRAY def read(self, binf): self.name = binf.streamString4() self.type = binf.stream1("i") print self.name, self.type if self.type == DOUBLEARRAY: self.values = binf.stream("%dd" % self.cParticles) # eg, stream("128d") elif self.type == VECTORARRAY: self.values = [ binf.stream("3d") for i in range(self.cParticles) ] else: assert False, "Unknown type: %d" % self.type def write(self, binf): binf.streamString4(self.name) binf.stream1("i", self.type) if self.type == DOUBLEARRAY: binf.stream("%dd" % self.cParticles, self.values) if self.type == VECTORARRAY: for v in self.values: binf.stream("3d", v) # --------------------------------------------- # PDC Generation Example (Spherical Harmonics) # --------------------------------------------- def gen_sh_comparison(): cacheFile = CACHEPATHANDPREFIX + '.250.pdc' print 'writing->', cacheFile pdc = ParticleData(cacheFile) xoffset = 0 yoffset = 0 x_list = [] y_list = [] z_list = [] v_list = [] for l in range(0,8): yoffset = -1.3*l for m in range(-l,l+1): xoffset = 1.3*m print 'processing l:', l, 'm:', m x,y,z,v = gen_sh(l, m, xoffset, yoffset, 200, 200) x_list.extend(x) y_list.extend(y) z_list.extend(z) v_list.extend(v) for name, type in zip(PDC_ATTRS.keys(), PDC_ATTRS.values()): a = Attribute() a.type = type a.name = name if name == 'position': a.values=zip(x_list,y_list,z_list) elif name == 'rgbPP': a.values = zip(v_list,v_list,v_list) elif name == 'opacityPP': a.values = [.05 for i in x_list] elif name == 'spriteScaleXPP' or name == 'spriteScaleYPP': a.values = [.01 for i in x_list] a.cParticles = len(a.values) pdc.attribs.append(a) #update particle counts pdc.cParticles = len(pdc.attribs[0].values) pdc.cAttributes = len(PDC_ATTRS) pdc.write() print "Done processing %i particles" % pdc.cParticles @print_timing def gen_sh_series(): frame = 2 for l in range(0,8,1): for m in range(0,8,1): if m > l: continue print 'processing l:', l, 'm:', m gen_sh_single(l, m, frame) frame = frame + 1 def gen_sh_single(l=7, m=3, frame=1): cacheFile = CACHEPATHANDPREFIX + '.' + str(250 * frame) + '.pdc' print 'writing->', cacheFile pdc = ParticleData(cacheFile) x_list,y_list,z_list,v_list = gen_sh(l, m, 0, 0, 1500, 1500) #1500,1500 = 2.5 million points for name, type in zip(PDC_ATTRS.keys(), PDC_ATTRS.values()): a = Attribute() a.type = type a.name = name if name == 'position': a.values=zip(x_list,y_list,z_list) elif name == 'rgbPP': a.values = zip(v_list,v_list,v_list) elif name == 'opacityPP': a.values = [.05 for i in x_list] elif name == 'spriteScaleXPP' or name == 'spriteScaleYPP': a.values = [.01 for i in x_list] a.cParticles = len(a.values) pdc.attribs.append(a) #update particle counts pdc.cParticles = len(pdc.attribs[0].values) pdc.cAttributes = len(PDC_ATTRS) pdc.write() print "Done processing %i particles" % pdc.cParticles @print_timing def gen_sh(l,m, xoffset=0, yoffset=0, theta_resolution=200, phi_resolution=200): x_array = [] y_array = [] z_array = [] value_array = [] # generate random sample points theta_range = np.random.randn(theta_resolution) * 2 * math.pi phi_range = np.random.randn(phi_resolution) * 2 * math.pi for phi in phi_range: for theta in theta_range: # calculate sh val = abs(SH(l,m,theta,phi)) # only care about magnitude for pretty pictures value_array.append( val ) # transform spherical to cartesian x_array.append( val*np.outer(np.cos(phi), np.sin(theta)) + xoffset ) y_array.append( val*np.outer(np.sin(phi), np.sin(theta)) + yoffset ) z_array.append( val*np.outer(np.ones(phi.shape), np.cos(theta)) ) return (x_array, y_array, z_array, value_array) # --------------------------------------------- # Basic SH (should really be vectorized with numpy) # --------------------------------------------- def SH(l, m, theta, phi): """Basic sh function in spherical coordinates. Adapted from: http://www.cs.columbia.edu/~cs4162/slides/spherical-harmonic-lighting.pdf l is the band, range [0..N] m in the range [-l..l] theta in the range [0..Pi] phi in the range [0..2*Pi] """ if m==0: return K(1,0)*P(l,m,math.cos(theta)) elif m > 0: return math.sqrt(2.0)*K(l,m)*math.cos(m*phi)*P(l,m,math.cos(theta)) else: return math.sqrt(2.0)*K(l,-m)*math.sin(-m*phi)*P(l,-m,math.cos(theta)) def K(l, m): """renormalisation constant for SH function""" temp = ((2.0*l+1.0)*math.factorial(l-m)) / (4.0*math.pi*math.factorial(l+m)) return math.sqrt(temp) def P(l,m,x): """evaluate an Associated Legendre Polynomial P(l,m,x) at x""" pmm = 1.0 if m > 0: somx2 = ((1.0-x)*(1.0+x)) fact = 1.0 for i in xrange(1,m+1,1): pmm = pmm * (-fact) * somx2 fact = fact + 2 if l==m: return pmm pmmp1 = x * (2.0*m + 1.0) * pmm if l == (m+1): return pmmp1 pll = 0.0 for ll in xrange(m+2, l+1, 1): pll = ( (2.0*ll-1.0)*x*pmmp1-(ll+m-1.0)*pmm ) / (ll-m) pmm = pmmp1 pmmp1 = pll return pll # --------------------------------------------- # Binary I/O # --------------------------------------------- import struct class reader(object): """Wrapper around a read-only binary file""" def __init__(self, fileorname): """fileorname may be a file-like object or a filename""" self.byteorder = '=' # can also be '<' or '>' if hasattr(fileorname, 'read'): self.file = fileorname else: self.file = file(fileorname, 'rb') def read(self,len): return self.file.read(len) def stream(self, fmt, len=0): """Unpack fmt from f and return results.""" fmt = self.byteorder + fmt # don't enforce alignment! if len == 0: len = struct.calcsize(fmt) data = self.file.read(len) return struct.unpack(fmt, data) def stream1(self, fmt, len=0): """Unpack fmt from f and return just one result.""" (data,) = self.stream(fmt,len) return data def _streamStringLow(self, len): if len <= 0: return '' str = self.file.read(len-1) char = self.file.read(1) if ord(char)==0: return str else: return str+char def streamCooky(self): l = list(self.stream("4c")) l.reverse() return ''.join(l) def streamCookyNoReverse(self): l = list(self.stream("4c")) return ''.join(l) def streamString4(self): """Stream string prefixed with 4 bytes of length""" (strlen,) = self.stream("I",4) if strlen >= 2048: print "Bad string len: %#x" % strlen assert strlen < 2048 return self._streamStringLow(strlen) def streamString2(self): """Stream string prefixed with 2 bytes of length""" (strlen,) = self.stream("H",2) assert strlen < 2048 return self._streamStringLow(strlen) def streamString1(self): """Stream string prefixed with 1 byte of length""" (strlen,) = self.stream("B",1) assert strlen < 2048 return self._streamStringLow(strlen) class writer(object): """Wrapper around a write-only binary file""" def __init__(self, fileorname): """fileorname may be a file-like object or a filename""" if hasattr(fileorname, 'write'): self.file = fileorname else: self.file = file(fileorname, 'wb') def write(self,data): return self.file.write(data) def stream(self, fmt, *args): fmt = self.byteorder + fmt # add the "no alignment" flag to the fmt self.file.write(struct.pack(fmt, *args[0])) # for symmetry with reader def stream1(self, fmt, *args): fmt = self.byteorder + fmt # add the "no alignment" flag to the fmt self.file.write(struct.pack(fmt, *args)) def streamCooky(self, cooky): assert len(cooky)==4 l = list(cooky) l.reverse() self.write(struct.pack( self.byteorder + "4c", *l)) def streamCookyNoReverse(self, cooky): assert len(cooky)==4 l = list(cooky) self.write(struct.pack( self.byteorder + "4c", *l)) def streamString1(self, str): """Stream string prefixed with 1 byte of length""" #print "writing %s" % str # Game doesn't handle 0-length null strings self.file.write(struct.pack( self.byteorder + "B",len(str)+1)) self.file.write(str) self.file.write("\0") def streamString2(self, str): """Stream string prefixed with 2 bytes of length""" #print "writing %s" % str # Game doesn't handle 0-length null strings self.file.write(struct.pack( self.byteorder + "H",len(str)+1)) self.file.write(str) self.file.write("\0") def streamString4(self, str): """Stream string prefixed with 4 bytes of length""" if str=='': self.file.write(struct.pack( self.byteorder + "I",0)) else: #print "writing %s" % str self.file.write(struct.pack( self.byteorder + "I",len(str))) self.file.write(str) #self.file.write("\0") #what is this??? # --------------------------------------------- # Main # --------------------------------------------- def Main(args): for f in args: Process(f) if __name__=='__main__': input = sys.argv[1] if input == 'sh_series': gen_sh_series() elif input == 'sh_compare': gen_sh_comparison() elif input == 'sh_test': print 'sh', SH(4,2,10,20) print 'k', K(4,2) print 'p', P(4,2,10) else: print "Nothing to do..."