Skip to content

Commit ef49009

Browse files
authored
Merge pull request #7 from tbttfox/master
Make splitter command deal with .smpx files
2 parents 1950a09 + 6e79dd9 commit ef49009

File tree

1 file changed

+226
-29
lines changed

1 file changed

+226
-29
lines changed

scripts/SimplexUI/commands/simplexSplitter.py

Lines changed: 226 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
along with Simplex. If not, see <http://www.gnu.org/licenses/>.
1818
'''
1919

20-
#pylint:disable=redefined-outer-name, invalid-name
21-
"""
20+
'''
2221
Sides:
2322
L = Shows up in left side when splitX
2423
Shows up in UD when splitV
@@ -34,8 +33,12 @@
3433
V = Splittable into U/D
3534
3635
S = Symmetric: Internal use
37-
"""
38-
import json
36+
'''
37+
import json, pprint
38+
import numpy as np
39+
from alembic.Abc import V3fTPTraits, Int32TPTraits, OArchive, IArchive, OStringProperty
40+
from alembic.AbcGeom import OPolyMeshSchemaSample, OXform, IPolyMesh, IXform, OPolyMesh
41+
3942

4043
LEFTSIDE = "L"
4144
RIGHTSIDE = "R"
@@ -52,6 +55,10 @@
5255
SEP = "_"
5356
SYMMETRIC = "S"
5457

58+
59+
60+
61+
5562
class Falloff(object):
5663
def __init__(self, name, search, rep, foType, axis, foValues):
5764
self.name = name
@@ -62,6 +69,63 @@ def __init__(self, name, search, rep, foType, axis, foValues):
6269
self.axis = axis
6370
self.foValues = foValues
6471

72+
self._max = foValues[0]
73+
self._maxTan = foValues[1]
74+
self._minTan = foValues[2]
75+
self._min = foValues[3]
76+
77+
self._weights = None
78+
self._verts = None
79+
self._bezier = None
80+
81+
self.IMAG_COUNT = 0
82+
self.REAL_COUNT = 0
83+
84+
85+
@property
86+
def bezier(self):
87+
if self._bezier is None:
88+
# Based on method described at
89+
# http://edmund.birotanker.com/monotonic-bezier-curves-for-animation.html
90+
p0x = 0
91+
p1x = self._minTan
92+
p2x = self._maxTan
93+
p3x = 1
94+
95+
f = (p1x - p0x)
96+
g = (p3x - p2x)
97+
d = 3*f + 3*g - 2
98+
n = 2*f + g - 1
99+
r = (n*n - f*d) / (d*d)
100+
qq = ((3*f*d*n - 2*n*n*n) / (d*d*d))
101+
self._bezier = (qq, r, d, n)
102+
return self._bezier
103+
104+
def getMultiplier(self, xVal):
105+
# Vertices are assumed to be at (0,0) and (1,1)
106+
if xVal <= self._min:
107+
return 0.0
108+
if xVal >= self._max:
109+
return 1.0
110+
111+
tVal = float(xVal - self._min) / float(self._max - self._min)
112+
qq, r, d, n = self.bezier
113+
q = qq - tVal/d
114+
discriminant = q*q - 4*r*r*r
115+
if discriminant >= 0:
116+
self.REAL_COUNT += 1
117+
pm = (discriminant**0.5)/2
118+
w = (-q/2 + pm)**(1/3.0)
119+
u = w + r/w
120+
else:
121+
self.IMAG_COUNT += 1
122+
theta = math.acos(-q / ( 2*r**(3/2.0)) )
123+
phi = theta/3 + 4*math.pi/3
124+
u = 2 * r**(0.5) * math.cos(phi)
125+
t = u + n/d
126+
t1 = 1-t
127+
return (3*t1*t**2*1 + t**3*1)
128+
65129
def __hash__(self):
66130
return hash(self.name)
67131

@@ -70,12 +134,15 @@ def getSidedName(self, name, sIdx):
70134
s = "{0}{1}{0}".format(SEP, self.search)
71135
r = "{0}{1}{0}".format(SEP, self.rep[sIdx])
72136
nn = nn.replace(s, r)
137+
73138
s = "{0}{1}".format(SEP, self.search) # handle Postfix
74139
r = "{0}{1}".format(SEP, self.rep[sIdx])
75140
nn = nn.replace(s, r)
141+
76142
s = "{1}{0}".format(SEP, self.search) # handle Prefix
77143
r = "{1}{0}".format(SEP, self.rep[sIdx])
78144
nn = nn.replace(s, r)
145+
79146
return nn
80147

81148
@classmethod
@@ -96,6 +163,22 @@ def toJSON(self):
96163
out = [self.name, self.foType, self.axis]
97164
out.extend(self.foValues)
98165
return out
166+
167+
def setVerts(self, verts):
168+
self._verts = verts
169+
170+
@property
171+
def weights(self):
172+
if self._weights is None:
173+
if self.axis.lower() == HORIZONTALAXIS.lower():
174+
component = 0
175+
elif self.axis.lower() == VERTICALAXIS.lower():
176+
component = 1
177+
else:
178+
raise RuntimeError("Non-Planar Falloff found")
179+
w = [self.getMultiplier(v[component]) for v in self._verts]
180+
self._weights = np.array(w)
181+
return self._weights
99182

100183

101184
class Shape(object):
@@ -105,6 +188,7 @@ def __init__(self, name, side, maps=None, mapSides=None, oName=None):
105188
self.side = side
106189
self.maps = maps or []
107190
self.mapSides = mapSides or []
191+
self._verts = None
108192

109193
def canSplit(self, split):
110194
return split.search in self.side
@@ -123,7 +207,9 @@ def setSide(self, split, sIdx):
123207
ms.append(split.rep[sIdx])
124208
nn = split.getSidedName(self.name, sIdx)
125209

126-
return Shape(nn, newSide, newMaps, ms, self.oName)
210+
shp = Shape(nn, newSide, newMaps, ms, self.oName)
211+
shp._verts = self._verts
212+
return shp
127213

128214
@classmethod
129215
def loadJSON(cls, js):
@@ -139,21 +225,39 @@ def myprint(self, prefix):
139225
print prefix + self.name
140226
print prefix + str(self.maps)
141227

142-
def applyMapToObj(self):
143-
if not self.mapSides:
144-
return
145-
146-
print "Copying", self.oName, "To", self.name
147-
mapNames = ', '.join([i.name for i in self.maps])
148-
print "Applying maps:", mapNames
149-
print "With Sides:", self.mapSides
150-
print "\n"
151-
152228
def getPerMap(self, perMap):
153229
# make a dict keyed on the map and mapsides
154230
key = (tuple(self.maps), tuple(self.mapSides))
155231
perMap.setdefault(key, []).append(self)
156232

233+
def applyWeights(self, rest):
234+
rawWeights = np.ones(len(self._verts))
235+
axisPairs = [(VERTICALAXIS, TOPSIDE), (HORIZONTALAXIS, RIGHTSIDE)]
236+
for fo, sides in zip(self.maps, self.mapSides):
237+
weights = fo.weights
238+
for axis, side in axisPairs:
239+
if fo.axis.lower() == axis.lower():
240+
if side.lower() in sides.lower():
241+
weights = 1 - weights
242+
rawWeights *= weights
243+
244+
weightedDeltas = (self._verts - rest) * rawWeights[:, None]
245+
self._verts = rest + weightedDeltas
246+
247+
def loadSMPX(self, sample):
248+
vertexArray = []
249+
for j in xrange(len(sample)):
250+
fp = (sample[j][0], sample[j][1], sample[j][2])
251+
vertexArray.append(fp)
252+
self._verts = np.array(vertexArray)
253+
254+
def toSMPX(self):
255+
# Build the special alembic vert-table
256+
vertices = V3fTPTraits.arrayType(len(self._verts))
257+
for i, v in enumerate(self._verts):
258+
vertices[i] = tuple(v)
259+
return vertices
260+
157261

158262
class Progression(object):
159263
def __init__(self, name, shapes, times, interp, falloffs, forceSide=None):
@@ -314,7 +418,7 @@ def split(self, splitMap, splitList):
314418
if curside == SYMMETRIC:
315419
symState = dict(zip(lState, self.values) + zip(rState, self.values))
316420
symSliders, symValues = zip(*symState)
317-
symCombo = Combo(self.name, self.prog, symSliders, symValues)
421+
symCombo = Combo(self.name, self.prog, symSliders, symValues, self.groupIdx)
318422
return [symCombo]
319423

320424
nnL = splitMap.getSidedName(self.name, 0)
@@ -363,8 +467,28 @@ def __init__(self, name, groups, encodingVersion, clusterName, falloffs, shapes,
363467
self.combos = combos
364468
self.restShape = shapes[0]
365469
self._split = False
470+
self._smpx = None
471+
self._faces = None
472+
self._counts = None
473+
474+
def setDefaultComboProgFalloffs(self):
475+
for combo in self.combos:
476+
if combo.prog.falloffs:
477+
# falloff has been manually set
478+
continue
479+
480+
sliderSplits = []
481+
for slider in combo.sliders:
482+
sliderSplits.extend(slider.prog.falloffs)
483+
484+
if sliderSplits:
485+
# get the narrowest one
486+
idxs = [self.falloffs.index(i) for i in sliderSplits]
487+
split = self.falloffs[max(idxs)]
488+
combo.prog.falloffs = [split]
366489

367490
def split(self):
491+
self.setDefaultComboProgFalloffs()
368492
for falloff in self.falloffs:
369493
newsliders = []
370494
newcombos = []
@@ -405,14 +529,15 @@ def split(self):
405529
shapes = list(shapes)
406530
shapes.sort(key=lambda x: x.name)
407531
self.shapes = [self.restShape] + shapes
532+
408533
self._split = True
534+
if self._smpx:
535+
rest = self.restShape._verts
536+
for fo in self.falloffs:
537+
fo.setVerts(rest)
409538

410-
def applyMaps(self):
411-
if self._split:
412539
for shape in self.shapes:
413-
shape.applyMapToObj()
414-
else:
415-
raise RuntimeError("Please call the .split() method first")
540+
shape.applyWeights(rest)
416541

417542
def getPerMap(self):
418543
perMap = {}
@@ -445,6 +570,39 @@ def loadJSON(cls, js):
445570
combos = [Combo.loadJSON(i, progs, sliders) for i in jcombos]
446571
return cls(name, groups, encodingVersion, clusterName, falloffs, shapes, progs, sliders, combos)
447572

573+
@classmethod
574+
def loadSMPX(cls, smpx):
575+
iarch = IArchive(str(smpx)) # because alembic hates unicode
576+
try:
577+
top = iarch.getTop()
578+
par = top.children[0]
579+
par = IXform(top, par.getName())
580+
581+
abcMesh = par.children[0]
582+
abcMesh = IPolyMesh(par, abcMesh.getName())
583+
584+
systemSchema = par.getSchema()
585+
props = systemSchema.getUserProperties()
586+
prop = props.getProperty('simplex')
587+
jsString = prop.getValue()
588+
js = json.loads(jsString)
589+
system = cls.loadJSON(js)
590+
system._smpx = smpx
591+
meshSchema = abcMesh.getSchema()
592+
rawFaces = meshSchema.getFaceIndicesProperty().samples[0]
593+
rawCounts = meshSchema.getFaceCountsProperty().samples[0]
594+
595+
system._faces = [i for i in rawFaces]
596+
system._counts = [i for i in rawCounts]
597+
598+
for shape, sample in zip(system.shapes, meshSchema.getPositionsProperty().samples):
599+
shape.loadSMPX(sample)
600+
601+
finally:
602+
del iarch
603+
604+
return system
605+
448606
def toJSON(self):
449607
name = self.name
450608
groups = self.groups
@@ -470,10 +628,40 @@ def toJSON(self):
470628

471629
return js
472630

631+
def toSMPX(self, path):
632+
defDict = self.toJSON()
633+
jsString = json.dumps(defDict)
634+
635+
arch = OArchive(str(path)) # alembic does not like unicode filepaths
636+
try:
637+
par = OXform(arch.getTop(), str(self.name))
638+
props = par.getSchema().getUserProperties()
639+
prop = OStringProperty(props, "simplex")
640+
prop.setValue(str(jsString))
641+
mesh = OPolyMesh(par, str(self.name))
642+
643+
faces = Int32TPTraits.arrayType(len(self._faces))
644+
for i, f in enumerate(self._faces):
645+
faces[i] = f
646+
647+
counts = Int32TPTraits.arrayType(len(self._counts))
648+
for i, c in enumerate(self._counts):
649+
counts[i] = c
650+
651+
schema = mesh.getSchema()
652+
for shape in self.shapes:
653+
verts = shape.toSMPX()
654+
abcSample = OPolyMeshSchemaSample(verts, faces, counts)
655+
schema.set(abcSample)
656+
except:
657+
raise
658+
659+
finally:
660+
del arch
661+
473662

474663
# I'm not using these b/c they're slower than just using json.dumps
475664
# Though when debugging, they're kinda nice to have
476-
import pprint
477665
def pprintFormatter(thing, context, maxlevels, level):
478666
typ = pprint._type(thing)
479667
if typ is unicode:
@@ -487,23 +675,32 @@ def makeHumanReadableDump(nestedDict):
487675
dump = dump.replace("'", '"')
488676
return dump
489677

490-
if __name__ == "__main__":
491-
jpath = 'C:\Users\tyler\Desktop\Simplex2\commands\test.json'
492-
opath = 'C:\Users\tyler\Desktop\Simplex2\commands\test_Split.json'
493678

679+
680+
681+
def test():
682+
jpath = r'C:\Users\tyler\Desktop\Simplex2\commands\test.json'
683+
opath = r'C:\Users\tyler\Desktop\Simplex2\commands\test_Split.json'
494684
with open(jpath) as f:
495685
j = json.loads(f.read())
496686
system = Simplex.loadJSON(j)
497687
system.split()
498688
js = system.toJSON()
499-
#hr = makeHumanReadableDump(js)
500-
#system.applyMaps()
501-
jsOut = json.dumps(js, sort_keys=True, indent=2, separators=(',', ':'))
502-
689+
jsOut = makeHumanReadableDump(js)
503690
with open(opath, 'w') as f:
504691
f.write(jsOut)
505692

506693

694+
if __name__ == "__main__":
695+
inPath = r'C:\Users\tyler\Desktop\HeadMaleStandard_Low_Unsplit.smpx'
696+
outPath = r'C:\Users\tyler\Desktop\HeadMaleStandard_Low_WOOT.smpx'
697+
698+
system = Simplex.loadSMPX(inPath)
699+
system.split()
700+
system.applySplit()
701+
system.toSMPX(outPath)
702+
print makeHumanReadableDump(system.toJSON())
703+
print "DONE"
507704

508705

509706

0 commit comments

Comments
 (0)