import poser, math, Numeric, os from Tkinter import * """ This script applies a smoothing process which drags morphed vertices back toward their distance relationships in the unmorphed mesh. This script was based on the Restore Details script written by Cage as part of the TDMT project. The above description is also the first line / description of that script but this one differs in the following (primary) way... Restore Details: uses the "average center" of the neighboring vertices as the target/restored position. Restore Relations: uses a target/restored position computed by a weighting of each of those neighboring vertices. ...this weighting is determined based on the distance to each neighboring vertex, using the same get_weights() routine from the current (at the time) TDMT closest-vertex correlation scripts. In general, this idea of a target/restore location is more accurate for restoring the relative relationships between the vertices (the average/center approach would really only be accurate for something like a grid mesh). Having said that, both methods are only approximations, since they only deal with a distance and don't take angles into account. The net affect of this is that they both tend to 'shrink' the mesh to some degree, but proper settings can mitigate this and the code has implemented methods to help with this as well. Free for any use. Problems, questions, or comments can be directed to member Spanki at www.renderosity.com. """ scene = poser.Scene() root = Tk() do_print = 0 FLT_EPSILON = 1.19209290e-07 def run(reps,threshold,morphs,weld,edge,zero,steps): app.status_update("Getting actor data....") theAct = scene.CurrentActor() theMesh = myMesh(theAct) # Instantiate the mesh theMesh.init_lists() theMesh.matchlist = [0 for i in theMesh.verts] if weld or edge or zero: theMesh.init_screen(weld,edge,zero) if morphs: # Mix in deltas for selected morphs mix_deltas(theAct,theMesh,morphs) find_relationships(theMesh) app.status_update("Starting mesh processing....") #================================================================================= # The code below is where the actual processing takes place... Note that it's # currently set up to do 4 passes through the restore_relationships() routine, # starting with 4xthreshold value, then 3x, then 2x and then the final pass with # the entered threshold value. # # The reason for doing multiple passes is that since each vertex depends on it's # neighboring vertices, the ones that are far out of whack can have a trickle-out # affect on the surrounding vertices. By running multiple passes with progressively # tighter tolerances, we can address the worse offending vertices in the earlier # passes, so that they are closer to final position before later passes with tighter # tolerances (when more verts will be adjusted). #================================================================================= restore_relationships(theMesh,reps,threshold*4,steps) theMesh.matchlist = [0 for i in theMesh.verts] if weld or edge or zero: theMesh.init_screen(weld,edge,zero) restore_relationships(theMesh,reps,threshold*3,steps) theMesh.matchlist = [0 for i in theMesh.verts] if weld or edge or zero: theMesh.init_screen(weld,edge,zero) restore_relationships(theMesh,reps,threshold*2,steps) theMesh.matchlist = [0 for i in theMesh.verts] if weld or edge or zero: theMesh.init_screen(weld,edge,zero) restore_relationships(theMesh,reps,threshold,steps) #test_weights(theMesh) # for debugging... just shows weight-derived vert locations setShape(theMesh,theAct) def vertexPos(geom,worldspace=1,multiple=1.0): """ Pre-fetch vertex coordinates for faster access. Returns a Numeric array of vert coords by vert indices Send worldspace=0 keyword when calling function, to use localspace """ numVerts = geom.NumVertices() verts = [[0.0,0.0,0.0] for i in range(numVerts)] verts = Numeric.array(verts,Numeric.Float) for i in range(numVerts): if worldspace: v = geom.WorldVertex(i) else: v = geom.Vertex(i) verts[i] = [v.X(),v.Y(),v.Z()] return verts class myMesh(object): """Initialize the mesh data - just a container class for pyd Mesh instance""" def __init__(self,act): self.act = act self.geom = act.Geometry() self.numverts = self.geom.NumVertices() self.polyverts(self.geom,self.geom.Polygons()) pgons = self.pverts self.vertpolys(self.geom,pgons) self.vertneighbors(pgons) def init_lists(self): self.baseverts = vertexPos(self.geom,worldspace=0) # Used for worldspace removal in setShape self.verts = vertexPos(self.geom,worldspace=1) # Used for worldspace removal in setShape self.points = vertexPos(self.geom,worldspace=1) # Store hit locations to pass to post-processing functions self.weights = [[] for i in range(len(self.baseverts))] self.mindists = [1000.0 for i in range(len(self.baseverts))] # used for adaptive screening (initialized to large value) self.polyedges(self.pverts) self.no_neighbors() def init_screen(self,weld,edge,zero): if weld: self.screen_weld() if zero: self.screen_zeroes() if edge: self.screen_edge() def polyverts(self,geom,polys): """ vertices by polygon - this is the Sets() list broken down by polygon index. Returns a list of Numeric arrays """ self.pverts = [] gset = geom.Sets() for p in polys: l = p.NumVertices() s = p.Start() e = s + l vl = Numeric.array([v for v in gset[s:e]],Numeric.Int) self.pverts.append(vl) def vertneighbors(self,pgons): """ neighbor verts of verts pgons is self.pverts or self.tverts """ self.vneighbors = [[] for i in range(len(self.vpolys))] for vi in range(len(self.vpolys)): buflist = [vi] # Need to exclude the vert itself! for ps in self.vpolys[vi]: for v in pgons[ps]: if not v in buflist: self.vneighbors[vi].append(v) buflist.append(v) self.vneighbors = [Numeric.array(j,Numeric.Int) for j in self.vneighbors] def vertpolys(self,geom,pgons): """ polygons by vertex - each vert will have multiple listings returns a list of Numeric arrays """ self.vpolys = [[] for i in range(geom.NumVertices())] for pvi in range(len(pgons)): for v in pgons[pvi]: if not pvi in self.vpolys[v]: self.vpolys[v].append(pvi) self.vpolys = [Numeric.array(i,Numeric.Int) for i in self.vpolys] def polyedges(self,pgons): """ edges of polygons as [start vert,end vert] returns a Numeric array """ self.edges = [[[0,0] for j in range(len(i))] for i in pgons] for p in range(len(pgons)): for v in range(len(pgons[p])): if v != len(pgons[p])-1: self.edges[p][v] = [pgons[p][v],pgons[p][v+1]] else: self.edges[p][v] = [pgons[p][v],pgons[p][0]] self.edges = [Numeric.array(j,Numeric.Int) for j in self.edges] def no_neighbors(self,group=None,reverse=0,useV=1,useE=0): """ Locate edges of polys which have no polygon neighbors noNeighbors lists edges by ngon, in the format of the edges list noNeighborsV converts the same data to vertices. noNeighborsE lists the clockwise (reverse is counter-clockwise) edge partner for each of these verts Used by screen_vedges() If submitted, group will restrict returns to polys within that group """ pgons = self.geom.NumPolygons() verts = self.numverts self.noNeighbors = [[-1 for j in i] for i in self.edges] # Result will be -1 for split edges, neighbor poly index for not. for pi in range(pgons): for e1,e2 in self.edges[pi]: for p in self.vpolys[e1]: if p != pi: if group == None or self.geom.Polygon(p).InGroup(group): for edge in range(len(self.edges[p])): e3,e4 = self.edges[p][edge] if (e1,e2) == (e4,e3): self.noNeighbors[p][edge] = pi if group != None: if not self.geom.Polygon(p).InGroup(group): for edge in range(len(self.edges[p])): self.noNeighbors[p][edge] = pi if useE or useV: if useV: self.noNeighborsV = [0 for i in range(verts)] # Will contain 1 for split edges, 0 for not. if useE: self.noNeighborsE = [-1 for i in range(verts)] if reverse == 2: self.noNeighborsE = [[] for i in range(verts)] for edge in range(len(self.edges)): for verts2 in range(len(self.edges[edge])): if self.noNeighbors[edge][verts2] == -1: if useV: for vi in range(2): self.noNeighborsV[self.edges[edge][verts2][vi]] = 1 if useE: if reverse == 1: self.noNeighborsE[self.edges[edge][verts2][1]] = self.edges[edge][verts2][0] if reverse == 0: self.noNeighborsE[self.edges[edge][verts2][0]] = self.edges[edge][verts2][1] if reverse == 2: self.noNeighborsE[self.edges[edge][verts2][0]].append([self.edges[edge][verts2][0],self.edges[edge][verts2][1]]) self.noNeighborsE[self.edges[edge][verts2][1]].append([self.edges[edge][verts2][0],self.edges[edge][verts2][1]]) def screen_edge(self): """ Screen edges with no polygon neighbors. This will locate split edges, including weld edges on body part actors. """ self.no_neighbors() for vi in range(self.numverts): if self.noNeighborsV[vi]: # 1 is split; 0 is not. self.matchlist[vi] = 1 def screen_weld(self): if not self.act.IsBodyPart(): return acts = self.act.Children() acts.append(self.act) for act in acts: if act.Name() == self.act.Name(): welds = [[j for j in range(len(i)) if i[j] != -1] for i in act.WeldGoals()] else: welds = [[j for j in i if j != -1] for i in act.WeldGoals()] for j in welds: for i in j: self.matchlist[i] = 1 def screen_zeroes(self): """ Screens out verts in current shape if their positions match those of the base geometry. """ for vi in range(self.numverts): d = self.verts[vi] - self.baseverts[vi] if abs(d) < FLT_EPSILON: self.matchlist[vi] = 1 #=============================================================================================== # mix_deltas() #=============================================================================================== def mix_deltas(act,mesh,morphs): for name in morphs: mt = act.Parameter(name) for vi in range(mesh.geom.NumVertices()): d = mt.MorphTargetDelta(vi) if (abs(d[0]) > FLT_EPSILON) or (abs(d[1]) > FLT_EPSILON) or (abs(d[2]) > FLT_EPSILON): for i in range(3): mesh.points[vi][i] += d[i] #=============================================================================================== # get_weights() #=============================================================================================== def get_weights(dists): """Calculate weights for correlated vertices, by distance.""" for i in range(len(dists)): if dists[i] <= FLT_EPSILON: # Float division error protection. return [1.0/len(dists) for i in dists] total = 0.0 result = [0.0 for i in dists] for i in range(len(dists)): result[i] = 1.0 / dists[i] total += result[i] if total <= FLT_EPSILON: # Float division error protection. return [1.0/len(dists) for i in dists] for i in range(len(result)): result[i] = result[i] / total return result #=============================================================================================== # test_weights() #=============================================================================================== def test_weights(mesh): """ This is just a debugging routine to test the validity of the get_weights() routine above. If you enable this (up in the main run() routine), the morph created will just be a visual representation of the weight-derived vert locations... you can use this to see the slight 'shrinking' affect that I discussed in the header. """ for vi in range(len(mesh.baseverts)): nbs = mesh.vneighbors[vi] numNbs = len(nbs) newpoint = [0.0,0.0,0.0] total = 0.0 for ni in range(numNbs): # compute where this vertex 'should' be... nvi = nbs[ni] nv = mesh.baseverts[nvi] weight = mesh.weights[vi][ni] total += weight newpoint[0] += nv[0] * weight newpoint[1] += nv[1] * weight newpoint[2] += nv[2] * weight mesh.points[vi][0] = newpoint[0] mesh.points[vi][1] = newpoint[1] mesh.points[vi][2] = newpoint[2] #vert = mesh.baseverts[vi] #disp = "Vert: [%.4f, %.4f, %.4f] Computed: [%.4f, %.4f, %.4f]" %(vert[0], vert[1], vert[2], newpoint[0], newpoint[1], newpoint[2]) #print disp #print "Vert: %s Total: %.8f" %(vi, total) # this print out is no longer needed... total == 1.0 in every case (get_weights() code validated) #=============================================================================================== # find_relationships() #=============================================================================================== def find_relationships(mesh): """ Determines weight values for all neighbor vertices for each vertex. The weights represent the values needed to re-generate each vertex, relative to it's neighbors, but in it's new (morphed) configuration. These weights are used by restore_detail to try to reconstruct the basic vertex relationships after morphing (or shrink-wrapping). """ for vi in range(len(mesh.baseverts)): nbs = mesh.vneighbors[vi] numNbs = len(nbs) nbdists = [0.0 for i in range(numNbs)] vert = mesh.baseverts[vi] for i in range(numNbs): nvi = nbs[i] nv = mesh.baseverts[nvi] vdelta = nv - vert nbdists[i] = Numeric.sqrt( pow(vdelta[0],2) + pow(vdelta[1],2) + pow(vdelta[2],2) ) mesh.mindists[vi] = min(nbdists) # store the minimum distance for adaptive screening weights = get_weights(nbdists) # get the weights for each neighbor vert for i in range(numNbs): mesh.weights[vi].append(weights[i]) # ...and store them for this vertex #=============================================================================================== # restore_relationships() #=============================================================================================== def restore_relationships(mesh,reps,threshold,steps=20): #(mesh,reps=1000,threshold=0.0015) #reps = Maximum number of looped repititions for function #threshold = difference in distance between base geom and adjusted shape at which script will stop affecting a vert oldcount = mesh.matchlist.count(1) for rep in range(reps): points = mesh.points for vi in range(len(mesh.points)): if not mesh.matchlist[vi]: # make sure that the vert is still being processed vert = mesh.points[vi] nbs = mesh.vneighbors[vi] numNbs = len(nbs) vadjust = [0.0,0.0,0.0] vadjust = Numeric.array(vadjust,Numeric.Float) for ni in range(numNbs): # compute where this vertex 'should' be, based on it's neighbors... nvi = nbs[ni] nv = mesh.points[nvi] weight = mesh.weights[vi][ni] vadjust[0] += nv[0] * weight vadjust[1] += nv[1] * weight vadjust[2] += nv[2] * weight vdelta = vadjust - vert # now see how far it currently is, from where it should be dist = Numeric.sqrt( pow(vdelta[0],2) + pow(vdelta[1],2) + pow(vdelta[2],2) ) #================================================================================= # The test below is still under development (but seems to be helping as-is). # # The basic idea is an 'adaptive screening', based on the 'density' of the mesh # surrounding each vertex... # # The problem is that for tight tolerences, we continue to degrade (shrink) the # parts of the mesh that are made up of large polygons (the ones that tend to # shink the most). It's not important to restore those to such tight tolerances # as the smaller/denser mesh areas, so we mark them as 'done' earlier in the # process. # # The implementation is (as usual) based on an approximation of mesh density, which # in this case is just based on the distance to it's closest neighbor vert (which # was determined back in find_relationships()). #================================================================================= if (dist < threshold) or (mesh.mindists[vi] > (dist*8)): #if (dist < threshold): mesh.matchlist[vi] = 1 #================================================================================= # ...As for the odd bit of code below, what I'm basically doing is instead of just # moving the vert to where it should be (vadjust), I'm just moving it a little bit # in that direction each pass. This is done by averaging one part vadjust with # [steps] parts of the current location. # # The _reason_ for doing this is that each vert 'depends' on it's neighbors, which # in turn depend on thier neighbors, etc. By doing smaller adjustments, we minimize # the impact that one stray vert has and the surrounding verts can get 'locked down' # earlier in the process. Anyway, the 'steps' value is settable in the interface, # so the largest movement would be one step, which would average with the current # location, resulting in moving half-way to the target location. I have also used # step values of up to 100 or more, which seems to help in dense mesh areas. #================================================================================= else: for i in range(steps): vadjust += vert vadjust[0] = vadjust[0] / (steps+1) vadjust[1] = vadjust[1] / (steps+1) vadjust[2] = vadjust[2] / (steps+1) mesh.points[vi] = vadjust #print rep,mesh.matchlist.count(1),oldcount app.status_update("Running repetition %s..." %(rep)) if mesh.matchlist.count(1) == oldcount: # Quit once we reach the point where passes don't change the count - point of diminishing returns break oldcount = mesh.matchlist.count(1) #=============================================================================================== # setShape() #=============================================================================================== def setShape(mesh,theAct,addDeltas=0): # Set the morphs or bake the shape """create the morphs""" morphs = {} if addDeltas: deltas = [[0.0,0.0,0.0] for i in mesh.verts] for parm in theAct.Parameters(): if parm.IsMorphTarget(): morphs[parm.Name()] = parm.Value() if addDeltas: if parm.Value() <> 0.0: pv = parm.Value() for vi in range(mesh.mesh.NumVertices()): md = [i for i in parm.MorphTargetDeltas(vi)] for i in range(3): deltas[vi][i] + (md[i] * pv) parm.SetValue(0.0) mtname = unique_name("restore_relations",theAct,"") theAct.SpawnTarget(mtname) newMT = theAct.Parameter(mtname) newMT.SetValue(1.0) for parmname in morphs.keys(): parm = theAct.Parameter(parmname) parm.SetValue(morphs[parmname]) for vi in range(len(mesh.points)): vec = [0.0,0.0,0.0] for i in range(3): vec[i] = ((mesh.points[vi][i] - mesh.baseverts[vi][i]) - (mesh.verts[vi][i] - mesh.baseverts[vi][i])) if addDeltas: # Optionally add morph settings back in for i in range(3): vec[i] += deltas[vi][i] if vec: newMT.SetMorphTargetDelta(vi,vec[0],vec[1],vec[2]) scene.ProcessSomeEvents() scene.DrawAll() #=============================================================================================== # unique_name() #=============================================================================================== def unique_name(morphName,act,suffix,internal=0): """ Keep dial naming for the new morph from being automatically changed by Poser """ #Morph dial names max out at 30 characters, after which Poser #won't recognize the dial as a morph (?!?) if len(morphName) >= 20 and suffix != "": morphName = morphName[0:12] if suffix != "" and morphName.find(suffix) == -1: morphName = "%s%s" %(morphName,suffix) # Check the dial name to avoid duplication if internal: parms = [p.InternalName() for p in act.Parameters()] else: parms = [p.Name() for p in act.Parameters()] if morphName in parms: temp = 1 while "%s_%i" %(morphName,temp) in parms: temp += 1 # Name the target morph dial morphName = "%s_%i" %(morphName,temp) return morphName class App: def __init__(self, master): self.master = master master.title("Restore Relations") self.weldVar = IntVar() self.weldVar.set(1) self.edgeVar = IntVar() self.edgeVar.set(1) self.zeroVar = IntVar() self.zeroVar.set(0) self.masterFrame = Frame(self.master,borderwidth=2,relief=RIDGE) self.masterFrame.grid(row=1,column=0) self.mixFrame = Frame(self.masterFrame,borderwidth=2,relief=RIDGE) self.mixFrame.grid(row=0,column=0) self.screenFrame = Frame(self.mixFrame,borderwidth=2,relief=RIDGE) self.screenFrame.grid(row=0,column=0, sticky=E+W) self.screenLabel = Label(self.screenFrame, text="Screening Options" , anchor=N, justify=LEFT) self.screenLabel.grid(row=0, column=0, pady=2) self.checkFrame = Frame(self.screenFrame) self.checkFrame.grid(row=1,column=0, sticky=E+W) #self.screenLabel2 = Label(self.screenFrame, text=" " , anchor=N, justify=LEFT) #self.screenLabel2.grid(row=2, column=0) self.buttonFrame = Frame(self.mixFrame,borderwidth=2,relief=RIDGE) self.buttonFrame.grid(row=2,column=0) self.scriptLabel = Label(self.buttonFrame, text="Script Settings" , anchor=N, justify=LEFT) self.scriptLabel.grid(row=0, column=0, pady=2) #self.checkFrame2 = Frame(self.buttonFrame) #self.checkFrame2.grid(row=1,column=0, sticky=W) #self.radioFrame = Frame(self.buttonFrame) #self.radioFrame.grid(row=2,column=0) self.entryFrame = Frame(self.buttonFrame) self.entryFrame.grid(row=3,column=0, sticky=W) self.scriptLabel2 = Label(self.buttonFrame, text=" " , anchor=N, justify=LEFT) self.scriptLabel2.grid(row=4, column=0) self.quitFrame = Frame(self.mixFrame) self.quitFrame.grid(row=3,column=0) self.ListFrame2 = Frame(self.masterFrame,borderwidth=2,relief=RIDGE) self.ListFrame2.grid(row=0,column=1) self.displayFrame = Frame(self.master) self.displayFrame.grid(row=2,column=0) # --- Radio buttons --- #self.radioHi = Radiobutton(self.radioFrame,text="Screen Low",variable=self.hiloVar,value=0) #self.radioHi.grid(row=0,column=0) #self.radioLo = Radiobutton(self.radioFrame,text="Screen High",variable=self.hiloVar,value=1) #self.radioLo.grid(row=0,column=1) # --- Checkbuttons --- self.weldCheck = Checkbutton(self.checkFrame,text="Omit Welds", variable=self.weldVar) self.weldCheck.grid(row = 1, column = 0, sticky=W) self.edgeCheck = Checkbutton(self.checkFrame,text="Omit Split Edges", variable=self.edgeVar) self.edgeCheck.grid(row = 2, column = 0, sticky=W) self.zeroCheck = Checkbutton(self.checkFrame,text="Omit Zero Deltas", variable=self.zeroVar) self.zeroCheck.grid(row = 3, column = 0, sticky=W) #self.scrCheck = Checkbutton(self.checkFrame2,text="Screen Matched Verts", variable=self.scrVar) #self.scrCheck.grid(row = 5, column = 0) # --- Listboxes --- self.listLabel2 = Label(self.ListFrame2, text="Select morph(s)" , anchor=N, justify=LEFT) self.listLabel2.grid(row=1, column=1) self.ListScroll2 = Scrollbar(self.ListFrame2, orient=VERTICAL) # Morphs self.ListScroll2.grid( row=2, column=0,sticky=N+S+E) self.List2 = Listbox(self.ListFrame2, height=20, width=30, selectmode=MULTIPLE,exportselection=0, yscrollcommand=self.ListScroll2.set) self.List2.grid( row=2, column=1) self.ListScroll2["command"] = self.List2.yview self.List5 = Listbox(self.displayFrame, height=1, width=63, selectmode=SINGLE,exportselection=0) # Status bar self.List5.grid( row=3, column=0) # --- Entries --- self.cycsLabel = Label(self.entryFrame, text="Max Repetitions:" , anchor=N, justify=LEFT) self.cycsLabel.grid(row=0, column=0, sticky=W) self.entryCycs = Entry(self.entryFrame, width=10) self.entryCycs.insert(0,"1000") self.entryCycs.grid(row=0,column=1,padx=8) self.angLabel = Label(self.entryFrame, text="Threshold:" , anchor=N, justify=LEFT) self.angLabel.grid(row=1, column=0, sticky=W) self.entryAng = Entry(self.entryFrame, width=10) self.entryAng.insert(0,"0.00025") self.entryAng.grid(row=1,column=1,padx=8) self.stepsLabel = Label(self.entryFrame, text="Steps:" , anchor=N, justify=LEFT) self.stepsLabel.grid(row=2, column=0, sticky=W) self.entrySteps = Entry(self.entryFrame, width=10) self.entrySteps.insert(0,"50") self.entrySteps.grid(row=2,column=1,padx=8) # --- Buttons --- self.buttonRun = Button(self.quitFrame, text="Run", command=self.handleRun, padx=25) self.buttonRun.grid(row=0, column=0, padx=5, pady = 2) self.buttonQuit = Button(self.quitFrame, text="Quit", command=self.die, padx=25) self.buttonQuit.grid(row=0, column=3, padx=5, pady = 2) self.fill_boxes() self.status_update("Ready") def status_update(self,line): self.List5.insert(0,line) self.List5.update() def die(self): """End the script""" root.destroy() root.quit() def fill_boxes(self): act = scene.CurrentActor() for p in act.Parameters(): if p.IsMorphTarget(): self.List2.insert(END,p.Name()) def handleRun(self): s = self.List2.curselection() if s != (): morphs = [self.List2.get(int(c)) for c in s] else: morphs = [] reps = self.entryCycs.get() weld = self.weldVar.get() edge = self.edgeVar.get() zero = self.zeroVar.get() if edge: weld = 0 # Edge screening includes weld screening; no need to run both. try: reps = int(reps) except ValueError: reps = 1000 threshold = self.entryAng.get() try: threshold = float(threshold) except ValueError: threshold = 0.00025 steps = self.entrySteps.get() try: steps = int(steps) except ValueError: steps = 25 if steps < 1: steps = 1 run(reps,threshold,morphs,weld,edge,zero,steps) root.destroy() root.quit() app = App(root) root.mainloop()