Created
July 13, 2016 17:16
-
-
Save Farfarer/25e58a6523bf602cb98d11b8283717aa to your computer and use it in GitHub Desktop.
Install to "lxserv" directory in your script directory. Restart MODO. Use it by calling the command "ffr.combinevmaps"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import lx | |
import lxifc | |
import lxu.command | |
import lxu.select | |
default_type = 'weight' | |
default_source_name = 'Weight' | |
default_target_name = 'Weight' | |
source_type_value = default_type | |
target_type_value = default_type | |
map_channels = {} | |
map_channels['weight'] = ('Weight',) | |
map_channels['texture'] = ('U', 'V') | |
map_channels['rgb'] = ('Red', 'Green', 'Blue') | |
map_channels['rgba'] = ('Red', 'Green', 'Blue', 'Alpha') | |
map_channels['normal'] = ('X', 'Y', 'Z') | |
map_channels['morph'] = ('X', 'Y', 'Z') | |
map_channels['spot'] = ('X', 'Y', 'Z') | |
map_channels['position'] = ('X', 'Y', 'Z') | |
map_channels['vector'] = ('X', 'Y', 'Z') | |
map_channels['tbasis'] = ('Tangent X', 'Tangent Y', 'Tangent Z', 'Bitangent X', 'Bitangent Y', 'Bitangent Z') | |
blend_mode_unames = ('Normal', 'Add', 'Subtract', 'Difference', 'Normal Multiply', 'Divide', 'Multiply', 'Screen', 'Overlay', 'Soft Light', 'Hard Light', 'Darken', 'Lighten', 'Dodge', 'Burn') | |
blend_mode_names = ('normal', 'add', 'subtract', 'difference', 'normalmultiply', 'divide', 'multiply', 'screen', 'overlay', 'softlight', 'hardlight', 'darken', 'lighten', 'dodge', 'burn') | |
#_______________________________________________________________________________________________________________________ BLEND | |
def blendValues (o, i, mode): | |
if mode == 0: # Normal | |
return i | |
elif mode == 1: # Add | |
return o + i | |
elif mode == 2: # Subtract | |
return o - i | |
elif mode == 3: # Difference | |
return abs (o - i) | |
elif mode == 4: # Normal Multiply | |
return math.sqrt (abs (o * i)) | |
elif mode == 5: # Divide | |
return (((1.0 + i) / (1.0 + o)) - 0.5) * (1.0/1.5) | |
elif mode == 6: # Multiply | |
return i * o | |
elif mode == 7: # Screen | |
return 1.0 - ((1.0 - o) * (1.0 - i)) | |
elif mode == 8: # Overlay | |
return blend_overlay (o, i) | |
elif mode == 9: # Soft Light | |
return blend_softlight (o, i) | |
elif mode == 10: # Hard Light | |
return blend_hardlight (o, i) | |
elif mode == 11: # Darken | |
return min (o, i) | |
elif mode == 12: # Lighten | |
return max (o, i) | |
elif mode == 13: # Dodge | |
return blend_dodge (o, i) | |
elif mode == 14: # Burn | |
return blend_burn (o, i) | |
def blend_overlay (o, i): | |
if (2 * o) < 1: | |
return 2.0 * i * o | |
else: | |
if o < 1.0 and i < 1.0: | |
return 1.0 - 2.0 * (1.0 - o) * (1.0 - i) | |
else: | |
return 1.0 | |
def blend_softlight (o, i): | |
if i < 0.5: | |
return 2.0 * o * i + o * o * (1.0 - 2.0 * i) | |
elif o >= 0.0: | |
return math.sqrt (o) * (2.0 * i - 1.0) + (2.0 * o) * (1.0 - i) | |
else: | |
return 0.0 | |
def blend_hardlight (o, i): | |
if 2.0 * i < 1.0: | |
2.0 * i * o | |
else: | |
if o < 1.0 and i < 1.0: | |
val = 2.0 * (1.0 - o) * (1.0 - i) | |
else: | |
val = 0.0 | |
return 1.0 - val | |
def blend_dodge (o, i): | |
if o >= 1.0: | |
return 1.0; | |
elif o == 0.0: | |
return 0.0; | |
elif i >= 1.0: | |
return 1.0 | |
else: | |
val = o / (1.0 - i) | |
if (val > 1.0): | |
val = 1.0 | |
return val | |
def blend_burn (o, i): | |
if o >= 1.0: | |
return 1.0 | |
elif i == 0.0: | |
return 0.0 | |
else: | |
val = 1.0 - (1.0 - o) / i | |
if (val < 0.0): | |
val = 0.0 | |
return val | |
#_______________________________________________________________________________________________________________________ UTILS | |
class ListMeshMaps(lxifc.Visitor): | |
def __init__(self): | |
self.meshmap = None | |
self.meshmaps = {} | |
def vis_Evaluate(self): | |
if self.meshmap: | |
maptype = self.meshmap.Type () | |
if maptype not in self.meshmaps.keys (): | |
self.meshmaps[maptype] = [] | |
self.meshmaps[maptype].append (self.meshmap.Name ()) | |
class OptionPopup(lxifc.UIValueHints): | |
def __init__(self, internal, user): | |
self._items_i = internal | |
self._items_u = user | |
self._item_count = min (len (internal), len (user)) | |
def uiv_Flags(self): | |
return lx.symbol.fVALHINT_POPUPS | |
def uiv_PopCount(self): | |
return self._item_count | |
def uiv_PopUserName(self,index): | |
return self._items_u[index] | |
def uiv_PopInternalName(self,index): | |
return self._items_i[index] | |
class OptionPopupBlend(lxifc.UIValueHints): | |
def __init__(self): | |
pass | |
def uiv_Flags(self): | |
return lx.symbol.fVALHINT_POPUPS | |
def uiv_PopCount(self): | |
return min (len (blend_mode_unames), len (blend_mode_names)) | |
def uiv_PopUserName(self,index): | |
return blend_mode_unames[index] | |
def uiv_PopInternalName(self,index): | |
return blend_mode_names[index] | |
class OptionPopupMapName(lxifc.UIValueHints): | |
def __init__(self, mapNames, target=False): | |
self.mapNames = mapNames | |
self.cui_svc = lx.service.ChannelUI () | |
self.target = target | |
def type_value(self): | |
if self.target: | |
val = target_type_value | |
else: | |
val = source_type_value | |
if val not in map_channels.keys (): | |
val = default_type | |
return val | |
def uiv_Flags(self): | |
return lx.symbol.fVALHINT_POPUPS | |
def uiv_PopCount(self): | |
type_val = self.type_value () | |
if type_val in self.mapNames.keys(): | |
return len(self.mapNames[type_val]) | |
return 0 | |
def uiv_PopUserName(self,index): | |
type_val = self.type_value () | |
if type_val == 'weight': | |
return self.cui_svc.MeshMapUserName (self.mapNames[type_val][index], 1) | |
return self.mapNames[type_val][index] | |
def uiv_PopInternalName(self,index): | |
return self.mapNames[self.type_value ()][index] | |
class OptionPopupChannel(lxifc.UIValueHints): | |
def __init__(self, target=False): | |
self.target = target | |
def type_value(self): | |
if self.target: | |
val = target_type_value | |
else: | |
val = source_type_value | |
if val not in map_channels.keys (): | |
val = default_type | |
return val | |
def uiv_Flags(self): | |
return lx.symbol.fVALHINT_POPUPS | |
def uiv_PopCount(self): | |
return len(map_channels[self.type_value ()]) | |
def uiv_PopUserName(self,index): | |
return map_channels[self.type_value ()][index] | |
def uiv_PopInternalName(self,index): | |
return str (index) | |
#_______________________________________________________________________________________________________________________ VISITORS | |
class TransferVMaps (lxifc.Visitor): | |
def __init__ (self, point, polygon, mode, source_vmap_id, target_vmap_id, source_vmap_channel, target_vmap_channel, target_is_disco, blendmode): | |
self.point = point | |
self.polygon = polygon | |
self.mode = mode | |
self.source_vmap = source_vmap_id | |
self.target_vmap = target_vmap_id | |
self.source_chan = source_vmap_channel | |
self.target_chan = target_vmap_channel | |
self.target_is_disco = target_is_disco | |
self.blendmode = blendmode | |
# 6 is the maximum dimension of any vmaps currently (tangent basis). | |
self.maxChans = 6 | |
self.default_vector_value = [0.0] * self.maxChans | |
self.source_value = lx.object.storage ('f', self.maxChans) | |
self.source_value.set (self.default_vector_value) | |
self.target_value = lx.object.storage ('f', self.maxChans) | |
self.target_value.set (self.default_vector_value) | |
self.temp_value = lx.object.storage ('f', self.maxChans) | |
self.temp_value.set (self.default_vector_value) | |
def source_into_target (self): | |
temp_target_value = list (self.target_value.get ()) | |
temp_source_value = self.source_value.get () | |
temp_target_value[self.target_chan] = blendValues (temp_target_value[self.target_chan], temp_source_value[self.source_chan], self.blendmode) | |
self.target_value.set (tuple (temp_target_value)) | |
def vis_Evaluate (self): | |
self.source_value.set (self.default_vector_value) | |
self.target_value.set (self.default_vector_value) | |
pointID = self.point.ID () | |
if self.source_vmap == 'position' or self.target_vmap == 'position': | |
pos = self.point.Pos () | |
poly_count = self.point.PolygonCount () | |
valid_poly_count = poly_count | |
# Disco values (if maps are disco capable). | |
if self.target_is_disco: | |
valid_poly_count = 0 | |
for x in xrange (poly_count): | |
if self.source_vmap == 'position': | |
self.source_value.set (pos) | |
else: | |
self.point.MapEvaluate (self.source_vmap, self.source_value) | |
self.polygon.Select (self.point.PolygonByIndex (x)) | |
if self.polygon.TestMarks (self.mode): | |
valid_poly_count += 1 | |
target_disco = (self.target_is_disco and self.polygon.MapValue (self.target_vmap, pointID, self.target_value)) | |
if target_disco: | |
self.polygon.MapValue (self.source_vmap, pointID, self.source_value) | |
self.source_into_target () | |
self.polygon.SetMapValue (pointID, self.target_vmap, self.target_value) | |
# Now the continuous value. | |
if self.source_vmap == 'position': | |
self.source_value.set (pos) | |
else: | |
self.point.MapEvaluate (self.source_vmap, self.source_value) | |
if self.target_vmap == 'position': | |
self.target_value.set (pos) | |
else: | |
self.point.MapEvaluate (self.target_vmap, self.target_value) | |
# If any polygons were locked/hidden/unselected and they don't hold a disco value already, make it disco (if it can). | |
if self.target_is_disco and valid_poly_count != poly_count: | |
for x in xrange (poly_count): | |
self.polygon.Select (self.point.PolygonByIndex (x)) | |
if not self.polygon.TestMarks (self.mode): | |
if not self.polygon.MapValue (self.target_vmap, pointID, self.temp_value): | |
self.polygon.SetMapValue (pointID, self.target_vmap, self.target_value) | |
# Now apply the proper continuous value. | |
self.source_into_target () | |
if self.target_vmap == 'position': | |
self.point.SetPos (self.target_value) | |
else: | |
self.point.SetMapValue (self.target_vmap, self.target_value) | |
#_______________________________________________________________________________________________________________________ COMMAND | |
class CombineVMaps_Cmd(lxu.command.BasicCommand): | |
def __init__(self): | |
lxu.command.BasicCommand.__init__(self) | |
self.dyna_Add ('sourcetype', lx.symbol.sTYPE_STRING) | |
self.basic_SetFlags (0, lx.symbol.fCMDARG_QUERY) | |
self.dyna_Add ('sourcename', lx.symbol.sTYPE_STRING) | |
self.basic_SetFlags (1, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_DYNAMICHINTS) | |
self.dyna_Add ('sourcechannel', lx.symbol.sTYPE_STRING) | |
self.basic_SetFlags (2, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_DYNAMICHINTS) | |
self.dyna_Add ('targettype', lx.symbol.sTYPE_STRING) | |
self.basic_SetFlags (3, lx.symbol.fCMDARG_QUERY) | |
self.dyna_Add ('targetname', lx.symbol.sTYPE_STRING) | |
self.basic_SetFlags (4, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_DYNAMICHINTS) | |
self.dyna_Add ('targetchannel', lx.symbol.sTYPE_STRING) | |
self.basic_SetFlags (5, lx.symbol.fCMDARG_QUERY | lx.symbol.fCMDARG_DYNAMICHINTS) | |
self.dyna_Add ('blendmode', lx.symbol.sTYPE_STRING) | |
self.basic_SetFlags (6, lx.symbol.fCMDARG_OPTIONAL | lx.symbol.fCMDARG_QUERY) | |
self.mesh_svc = lx.service.Mesh () | |
self.msg_svc = lx.service.Message () | |
self.layer_svc = lx.service.Layer () | |
vmap_itypes = [x for x in dir (lx.symbol) if x.startswith ('i_VMAP_')] # Grab vmap ID defines. | |
vmap_itypes = [getattr (lx.symbol, x) for x in vmap_itypes] # Get the value of the defines. | |
self.vmap_itypes = [x for x in vmap_itypes if self.mesh_svc.VMapDimension (x) > 0 and not self.mesh_svc.VMapIsEdgeMap (x)] # Build a list of all maps which aren't edge maps. | |
self.vmap_names = map (self.mesh_svc.VMapLookupName, self.vmap_itypes) # Get the internal names of those maps. | |
self.vmap_unames = map (self.getVMapUsername, self.vmap_names) # Get the user names of those maps. | |
self.notifier = None | |
self.maps = {} | |
for mapName in self.vmap_names: | |
self.maps[mapName] = [] | |
# Given a vertex map itnernal name, return the user name (or an empty string if there isn't a user name). | |
def getVMapUsername (self, name): | |
try: | |
return self.msg_svc.RawText ('vertMapType', name) | |
except: | |
return '' | |
def arg_UIHints (self, index, hints): | |
if index == 0: | |
hints.Label ('Source Type') | |
elif index == 1: | |
hints.Label ('Source Map') | |
elif index == 2: | |
hints.Label ('Source Component') | |
elif index == 3: | |
hints.Label ('Target Type') | |
elif index == 4: | |
hints.Label ('Target Map') | |
elif index == 5: | |
hints.Label ('Target Component') | |
elif index == 6: | |
hints.Label ('Blend Mode') | |
def arg_UIValueHints(self, index): | |
if index == 0: | |
return OptionPopup (self.vmap_names, self.vmap_unames) | |
elif index == 1: | |
return OptionPopupMapName (self.maps, False) | |
elif index == 2: | |
return OptionPopupChannel (False) | |
elif index == 3: | |
return OptionPopup (self.vmap_names, self.vmap_unames) | |
elif index == 4: | |
return OptionPopupMapName (self.maps, True) | |
elif index == 5: | |
return OptionPopupChannel (True) | |
elif index == 6: | |
return OptionPopupBlend () | |
def cmd_Query(self,index,vaQuery): | |
va = lx.object.ValueArray () | |
va.set (vaQuery) | |
if index == 0: | |
va.AddString (source_type_value) | |
elif index == 1: | |
va.AddString (default_source_name) | |
elif index == 2: | |
va.AddString ('0') | |
elif index == 3: | |
va.AddString (target_type_value) | |
elif index == 4: | |
va.AddString (default_target_name) | |
elif index == 5: | |
va.AddString ('0') | |
elif index == 6: | |
va.AddString (blend_mode_names[0]) | |
return lx.result.OK | |
def cmd_DialogArgChange(self, index): | |
if index == 0: | |
global source_type_value | |
source_type_value = self.dyna_String (0, source_type_value) | |
self.updateDefaultMapName (False) | |
elif index == 3: | |
global target_type_value | |
target_type_value = self.dyna_String (3, target_type_value) | |
self.updateDefaultMapName (True) | |
def cmd_UserName(self): | |
return 'Combine Vertex Values' | |
def cmd_Desc(self): | |
return 'Move or combine vertex values from one vertex map to another.' | |
def cmd_Tooltip(self): | |
return 'Move or combine vertex values from one vertex map to another.' | |
def cmd_Help(self): | |
return 'http://www.farfarer.com/' | |
def basic_ButtonName(self): | |
return 'Combine Vertex Values' | |
def cmd_Flags(self): | |
return lx.symbol.fCMD_MODEL | lx.symbol.fCMD_UNDO | |
def basic_Enable(self, msg): | |
return True | |
def cmd_DialogInit(self): | |
self.getMapsOfType () | |
def cmd_DialogFormatting(self): | |
return '0 1 2 - 3 4 5 - 6' | |
def cmd_Interact(self): | |
pass | |
#_______________________________________________________________________________________________ MAIN FUNCTION EXECUTION | |
def getMapsOfType(self): | |
vis = ListMeshMaps () | |
layer_scan = lx.object.LayerScan (self.layer_svc.ScanAllocate (lx.symbol.f_LAYERSCAN_ACTIVE)) | |
if layer_scan.test (): | |
layer_scan_count = layer_scan.Count () | |
for layer_idx in xrange (layer_scan_count): | |
mesh = layer_scan.MeshBase (layer_idx) | |
if mesh.test (): | |
meshmap = lx.object.MeshMap (mesh.MeshMapAccessor ()) | |
if meshmap.test (): | |
vis.meshmap = meshmap | |
meshmap.Enumerate (lx.symbol.iMARK_ANY, vis, 0) | |
for mapName, mapType in zip (self.vmap_names, self.vmap_itypes): | |
if mapName == 'position': | |
self.maps[mapName].append ('Position') | |
elif mapType in vis.meshmaps.keys(): | |
self.maps[mapName] += vis.meshmaps[mapType] | |
for m in self.maps.keys(): | |
self.maps[m] = list (sorted (set (self.maps[m]))) | |
global default_type, default_source_name, default_target_name, source_type_value, target_type_value | |
for m in self.maps.keys (): | |
if len (self.maps[m]) > 0: | |
default_type = m | |
source_type_value = m | |
target_type_value = m | |
default_source_name = self.maps[m][0] | |
default_target_name = self.maps[m][0] | |
break | |
def updateDefaultMapName(self, target=False): | |
if target: | |
if target_type_value in self.maps.keys (): | |
global default_target_name | |
if default_target_name not in self.maps[target_type_value]: | |
if len(self.maps[target_type_value]) > 0: | |
default_target_name = self.maps[target_type_value][0] | |
self.attr_SetString (4, default_target_name) | |
else: | |
if source_type_value in self.maps.keys (): | |
global default_source_name | |
if default_source_name not in self.maps[source_type_value]: | |
if len(self.maps[source_type_value]) > 0: | |
default_source_name = self.maps[source_type_value][0] | |
self.attr_SetString (1, default_source_name) | |
def basic_Execute(self, msg, flags): | |
source_vmap_type = self.dyna_String (0, '') | |
source_vmap_name = self.dyna_String (1, '') | |
source_vmap_channel = self.dyna_Int (2, -1) | |
target_vmap_type = self.dyna_String (3, '') | |
target_vmap_name = self.dyna_String (4, '') | |
target_vmap_channel = self.dyna_Int (5, -1) | |
blendmode_str = self.dyna_String (6, blend_mode_names[0]) | |
blendmode = 0 | |
if blendmode_str in blend_mode_names: | |
blendmode = blend_mode_names.index (blendmode_str) | |
try: | |
source_vmap_itype = self.mesh_svc.VMapLookupType (source_vmap_type) | |
except: | |
lx.out('Invalid source map type.') | |
return | |
else: | |
if self.mesh_svc.VMapIsEdgeMap (source_vmap_itype): | |
lx.out('Source map must be a vertex map, not an edge map.') | |
return | |
try: | |
target_vmap_itype = self.mesh_svc.VMapLookupType (target_vmap_type) | |
except: | |
lx.out('Invalid target map type.') | |
return | |
else: | |
if self.mesh_svc.VMapIsEdgeMap (target_vmap_itype): | |
lx.out('Target map must be a vertex map, not an edge map.') | |
return | |
if source_vmap_channel < 0 or source_vmap_channel >= self.mesh_svc.VMapDimension (source_vmap_itype): | |
lx.out('Invalid source channel.') | |
return | |
if target_vmap_channel < 0 or target_vmap_channel >= self.mesh_svc.VMapDimension (target_vmap_itype): | |
lx.out('Invalid target channel.') | |
return | |
target_is_disco = self.mesh_svc.VMapIsContinuous (target_vmap_itype) == False | |
layer_scan = lx.object.LayerScan (self.layer_svc.ScanAllocate (lx.symbol.f_LAYERSCAN_EDIT)) | |
if not layer_scan.test (): | |
return | |
layer_scan_count = layer_scan.Count () | |
if layer_scan_count == 0: | |
return | |
mode = self.mesh_svc.ModeCompose ('select', 'hide lock') | |
for layer_idx in xrange (layer_scan_count): | |
mesh = lx.object.Mesh (layer_scan.MeshEdit (layer_idx)) | |
if not mesh.test (): | |
continue | |
if mesh.PointCount () == 0: | |
continue | |
point = lx.object.Point (mesh.PointAccessor ()) | |
polygon = lx.object.Polygon (mesh.PolygonAccessor ()) | |
meshmap = lx.object.MeshMap (mesh.MeshMapAccessor ()) | |
if not point.test () or not polygon.test () or not meshmap.test (): | |
continue | |
if source_vmap_type == 'position': | |
source_vmap_id = 'position' | |
else: | |
try: | |
meshmap.SelectByName (source_vmap_itype, source_vmap_name) | |
except: | |
continue | |
else: | |
source_vmap_id = meshmap.ID () | |
if target_vmap_type == 'position': | |
target_vmap_id = 'position' | |
else: | |
try: | |
meshmap.SelectByName (target_vmap_itype, target_vmap_name) | |
except: | |
continue | |
else: | |
target_vmap_id = meshmap.ID () | |
visitor = TransferVMaps (point, polygon, mode, source_vmap_id, target_vmap_id, source_vmap_channel, target_vmap_channel, target_is_disco, blendmode) | |
point.Enumerate (mode, visitor, 0) | |
if target_vmap_type == 'position': | |
flags = lx.symbol.f_MESHEDIT_POSITION | |
else: | |
if target_vmap_type == 'texture': | |
flags = lx.symbol.f_MESHEDIT_MAP_UV | |
elif target_vmap_type == 'morph' or target_vmap_type == 'spot': | |
flags = lx.symbol.f_MESHEDIT_MAP_MORPH | |
else: | |
lx.symbol.f_MESHEDIT_MAP_OTHER | |
if target_is_disco: | |
flags |= lx.symbol.f_MESHEDIT_MAP_CONTINUITY | |
layer_scan.SetMeshChange (layer_idx, flags) | |
layer_scan.Apply () | |
#________________________________________________________________________________________ BLESS FUNCTION AS MODO COMMAND | |
lx.bless (CombineVMaps_Cmd, 'ffr.combinevmaps') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment