Files
KKBP_Importer/importing/postoperations.py
小煜 5d9e09e9c3 feat: Add KoikatsuBlenderPipeline addon with import/export functionality
- Add core addon files (__init__.py, KKPanel.py, preferences.py, common.py)
- Add import pipeline with armature, mesh, and material modification modules
- Add export pipeline with material baking and FBX preparation utilities
- Add material combiner tool for texture atlas generation and optimization
- Add extras utilities for animation, rigging, and asset management
- Add bone orientation data from better_fbx for accurate skeletal structure
- Add comprehensive documentation and wiki with multi-language support (EN, JP, ZH)
- Add animation library retargeting lists for ARP and Rokoko motion capture
- Add Rigify integration scripts for advanced rigging workflows
- Add shader file (KK Shader V8.0.blend) for material rendering
- Add manifest and license files for addon distribution
- Add changelog documenting version history and improvements
- Initialize complete Blender addon project for Koikatsu character import/export
2025-12-06 15:26:19 +08:00

566 lines
27 KiB
Python

# This file performs the following operations
# Hide all clothes except the first outfit (alts are always hidden)
# (Cycles) Applies Cycles conversion script
# (Eevee Mod) Applies Eevee Mod conversion script
# (Rigify) Applies Rigify conversion script
# (SFW) Runs SFW cleanup script
# Clean orphaned data as long as users = 0 and fake user = False
# Parts of cycles replacement was taken from https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/234
import bpy, traceback
from .. import common as c
class post_operations(bpy.types.Operator):
bl_idname = "kkbp.postoperations"
bl_label = bl_idname
bl_description = bl_idname
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
self.hide_unused_objects()
self.apply_cycles()
self.apply_eeveemod()
self.apply_rigify()
self.apply_sfw()
self.separate_meshes()
c.clean_orphaned_data()
c.set_viewport_shading('SOLID')
return {'FINISHED'}
except Exception as error:
c.handle_error(self, error)
return {"CANCELLED"}
# %% Main functions
def hide_unused_objects(self):
"""
Hides unused objects in the Blender scene based on certain conditions.
This method performs the following operations:
1. Ensures the armature is not hidden.
3. Hides all outfits except the one with the lowest ID.
4. Moves eyegags and tears into their own collection.
5. Always hides the rigged tongue if present.
6. Always hides the Bone Widgets collection.
"""
c.get_armature().hide_set(False)
#hide all outfits except the first one
#but don't hide the collection if separate by material is enabled
clothes_and_hair = c.get_outfits()
clothes_and_hair.extend(c.get_hairs())
outfit_ids = (int(c['id']) for c in clothes_and_hair if c.get('id'))
outfit_ids = list(set(outfit_ids))
for id in outfit_ids:
clothes_in_this_id = [c for c in clothes_and_hair if c.get('id') == str(id).zfill(2)]
c.move_and_hide_collection(clothes_in_this_id, 'Outfit ' + str(id).zfill(2) + ' ' + c.get_name(), hide = (id != min(outfit_ids) and bpy.context.scene.kkbp.categorize_dropdown != 'B'))
#put any clothes variations into their own collection
outfit_ids = (int(c['id']) for c in c.get_alts() if c.get('id'))
outfit_ids = list(set(outfit_ids))
for index, id in enumerate(outfit_ids):
clothes_in_this_id = [c for c in c.get_alts() if c.get('id') == str(id).zfill(2)]
c.switch(clothes_in_this_id[0], 'OBJECT')
#find the character index
character_collection_index = len(bpy.context.view_layer.layer_collection.children)-1
#find the index of the outfit collection
for i, child in enumerate(bpy.context.view_layer.layer_collection.children[character_collection_index].children):
if child.name == 'Outfit ' + str(id).zfill(2) + ' ' + c.get_name():
break
for ob in clothes_in_this_id:
ob.select_set(True)
bpy.context.view_layer.objects.active=ob
new_collection_name = 'Alts ' + str(id).zfill(2) + ' ' + c.get_name()
#extremely confusing move to under the clothes collection. Index is the outfit index + outfit collection index (starts at 1) + Scene collection (1) + + character collection index (usually 0) + 1
bpy.ops.object.move_to_collection(collection_index = (index+i) + 1 + (character_collection_index + 1), is_new = True, new_collection_name = new_collection_name)
#then hide the alts
child.children[0].exclude = True
#put the eyegags and tears into their own collection
face_objects = []
if c.get_gags():
face_objects.append(c.get_gags())
if c.get_tears():
face_objects.append(c.get_tears())
if face_objects:
c.move_and_hide_collection(face_objects, 'Tears and gag eyes ' + c.get_name(), hide = False)
#always hide the rigged tongue if present
if c.get_tongue():
c.move_and_hide_collection([c.get_tongue()], 'Rigged tongue ' + c.get_name(), hide = True)
#always hide the hitboxes collection
if bpy.data.collections.get('Hitboxes ' + c.get_name()):
c.switch(c.get_armature(), 'OBJECT')
for child in bpy.context.view_layer.layer_collection.children[0].children:
if ('Hitboxes ' + c.get_name()) in child.name:
child.exclude = True
#always hide the bone widgets collection
if bpy.data.collections.get('Bone Widgets'):
c.switch(c.get_armature(), 'OBJECT')
for child in bpy.context.view_layer.layer_collection.children[0].children:
if child.name == 'Bone Widgets':
child.exclude = True
def apply_cycles(self):
if not bpy.context.scene.kkbp.shader_dropdown in ['B', 'D']:
return
c.kklog('Applying Cycles adjustments...')
c.import_from_library_file('NodeTree', ['.Cycles', '.Cycles no shadows', '.Cycles Classic'], bpy.context.scene.kkbp.use_material_fake_user)
c.import_from_library_file('Image', ['Template: Black'], bpy.context.scene.kkbp.use_material_fake_user)
#remove outline modifier
for o in bpy.context.view_layer.objects:
for m in o.modifiers:
if(m.name == "Outline Modifier"):
m.show_viewport = False
m.show_render = False
####fix the eyelash mesh overlap
# deselect everything and make body active object
body = c.get_body()
bpy.ops.object.select_all(action='DESELECT')
body.select_set(True)
bpy.context.view_layer.objects.active=body
bpy.ops.object.mode_set(mode = 'EDIT')
# define some stuff
ops = bpy.ops
obj = ops.object
mesh = ops.mesh
context = bpy.context
object = context.object
# edit mode and deselect everything
obj.mode_set(mode='EDIT')
mesh.select_all(action='DESELECT')
# delete eyeline down verts and kage faces
object.active_material_index = 6
obj.material_slot_select()
mesh.delete(type='VERT')
object.active_material_index = 5
obj.material_slot_select()
mesh.delete(type='ONLY_FACE')
mesh.select_all(action='DESELECT')
ignore_list = [
'KK Eyebrows (mayuge) ' + c.get_name(),
'KK EyeL (hitomi) ' + c.get_name(),
'KK EyeR (hitomi) ' + c.get_name(),
'KK Eyeline up ' + c.get_name(),
'KK Eyewhites (sirome) ' + c.get_name()]
everything = [c.get_body()]
everything.extend(c.get_hairs())
everything.extend(c.get_alts())
everything.extend(c.get_outfits())
#add cycles node group
for object in everything:
for node_tree in [mat_slot.material.node_tree for mat_slot in object.material_slots if mat_slot.material.get('bake') and mat_slot.material.name not in ignore_list]:
nodes = node_tree.nodes
links = node_tree.links
if nodes.get('combine'):
nodes['combine'].node_tree = bpy.data.node_groups['.Cycles' if bpy.context.scene.kkbp.shader_dropdown == 'B' else '.Cycles Classic']
#setup the node links again because they break when you replace the node group
def relink(outnode, outport, innode, inport):
try:
links.new(nodes[outnode].outputs[outport], nodes[innode].inputs[inport])
except:
c.kklog(f'Could not link these nodes on tree: {node_tree.name} | {outnode}:{outport} to {innode}:{inport}')
relink('combine', 0, 'out', 0)
relink('light', 0, 'combine', 'Light colors')
relink('dark', 0, 'combine', 'Dark colors')
relink('textures', 'Main texture (alpha)', 'combine', 'Main texture (alpha)')
relink('textures', 'Alpha mask', 'combine', 'Alpha mask')
relink('textures', 'Alpha mask (alpha)', 'combine', 'Alpha mask (alpha)')
relink('textures', 'Alpha mask (custom)', 'combine', 'Alpha mask (custom)')
#Cycles makes missing images PINK (?!) instead of black for some reason and this screws with the shaders
#If an image is missing, fill it in with Template: Black
if nodes.get('textures'):
for image_node in [n for n in nodes['textures'].node_tree.nodes if n.type == 'TEX_IMAGE']:
if not image_node.image:
image_node.image = bpy.data.images['Template: Black']
#disable detail shine color too
if nodes.get('light'):
if nodes['light'].inputs.get('Detail intensity (shine)'):
nodes['light'].inputs['Detail intensity (shine)'].default_value = 0
if nodes.get('dark'):
if nodes['dark'].inputs.get('Detail intensity (shine)'):
nodes['dark'].inputs['Detail intensity (shine)'].default_value = 0
#remove linemask and blush on face material
if c.get_body():
face_material = [m.material for m in c.get_body().material_slots if 'KK Face' in m.material.name]
if face_material:
face_material[0].node_tree.nodes['light'].inputs['Linemask intensity'].default_value = 0
face_material[0].node_tree.nodes['dark'].inputs['Linemask intensity'].default_value = 0
face_material[0].node_tree.nodes['light'].inputs['Blush intensity'].default_value = 0
face_material[0].node_tree.nodes['dark'].inputs['Blush intensity'].default_value = 0
#set eyeline up and eyebrows as shadowless
shadowless_mats = [m.material for m in c.get_body().material_slots if 'KK Eyeline up' in m.material.name]
shadowless_mats.extend([m.material for m in c.get_body().material_slots if 'KK Eyebrows (mayuge)' in m.material.name])
for mat in shadowless_mats:
mat.node_tree.nodes['combine'].node_tree = bpy.data.node_groups['.Cycles no shadows']
nodes = mat.node_tree.nodes
links = mat.node_tree.links
def relink(outnode, outport, innode, inport):
try:
links.new(nodes[outnode].outputs[outport], nodes[innode].inputs[inport])
except:
c.kklog(f'Could not link these nodes on tree: {node_tree.name} | {outnode}:{outport} to {innode}:{inport}')
relink('combine', 0, 'out', 0)
relink('light', 0, 'combine', 'Light colors')
relink('dark', 0, 'combine', 'Dark colors')
relink('textures', 'Main texture (alpha)', 'combine', 'Main texture (alpha)')
relink('textures', 'Alpha mask', 'combine', 'Alpha mask')
relink('textures', 'Alpha mask (alpha)', 'combine', 'Alpha mask (alpha)')
relink('textures', 'Alpha mask (custom)', 'combine', 'Alpha mask (custom)')
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.preview_samples = 10
mesh.select_all(action='DESELECT')
obj.mode_set(mode='OBJECT')
def apply_eeveemod(self):
if not bpy.context.scene.kkbp.shader_dropdown == 'C':
return
c.import_from_library_file('NodeTree', ['.Eevee Mod', '.Eevee Mod (face)'], bpy.context.scene.kkbp.use_material_fake_user)
c.kklog('Applying Eevee Shader adjustments...')
#Import eevee mod node group and replace the combine colors group with the eevee mod group
ignore_list = [
'KK Eyebrows (mayuge) ' + c.get_name(),
'KK EyeL (hitomi) ' + c.get_name(),
'KK EyeR (hitomi) ' + c.get_name(),
'KK Eyeline up ' + c.get_name(),
'KK Eyewhites (sirome) ' + c.get_name()]
everything = [c.get_body()]
everything.extend(c.get_hairs())
everything.extend(c.get_alts())
everything.extend(c.get_outfits())
for object in everything:
for node_tree in [mat_slot.material.node_tree for mat_slot in object.material_slots if mat_slot.material.get('bake') and mat_slot.material.name not in ignore_list]:
nodes = node_tree.nodes
links = node_tree.links
if nodes.get('combine'):
nodes['combine'].node_tree = bpy.data.node_groups['.Eevee Mod']
#setup the node links again because they break when you replace the node group
def relink(outnode, outport, innode, inport):
try:
links.new(nodes[outnode].outputs[outport], nodes[innode].inputs[inport])
except:
c.kklog(f'Could not link these nodes on tree: {node_tree.name} | {outnode}:{outport} to {innode}:{inport}')
relink('combine', 0, 'out', 0)
relink('light', 0, 'combine', 'Light colors')
relink('dark', 0, 'combine', 'Dark colors')
relink('textures', 'Main texture (alpha)', 'combine', 'Main texture (alpha)')
relink('textures', 'Alpha mask', 'combine', 'Alpha mask')
relink('textures', 'Alpha mask (alpha)', 'combine', 'Alpha mask (alpha)')
relink('textures', 'Alpha mask (custom)', 'combine', 'Alpha mask (custom)')
if bpy.app.version[0] == 3:
#turn on ambient occlusion and bloom in render settings
bpy.context.scene.eevee.use_gtao = True
#turn on bloom in render settings
bpy.context.scene.eevee.use_bloom = True
#face has special normal setup. make a copy and add the normals inside of the copy
#this group prevents Amb Occ issues around nose, and mouth interior
face_nodes = bpy.data.node_groups['.Eevee Mod (face)']
face_nodes.use_fake_user = True
#select entire face and body, then reset vectors to prevent Amb Occ seam around the neck
body = c.get_body()
bpy.ops.object.select_all(action='DESELECT')
body.select_set(True)
bpy.context.view_layer.objects.active=body
bpy.ops.object.mode_set(mode = 'EDIT')
body.active_material_index = 1
bpy.ops.object.material_slot_select()
bpy.ops.mesh.normals_tools(mode='RESET')
bpy.ops.object.mode_set(mode = 'OBJECT')
@classmethod
def apply_rigify(cls):
self = cls
#correct some bone layering errors. I don't feel like tracking these down, so do it here before the rigify script
layer0_bones = [
'MasterFootIK.L',
'MasterFootIK.R',
'Eyesx',
'cf_pv_root_upper',
'cf_pv_elbo_R',
'cf_pv_elbo_L',
'cf_pv_knee_L',
'cf_pv_knee_R',
'cf_pv_hand_L',
'cf_pv_hand_R',
]
layer1_bones = [
'Left toe',
'Right toe',
'cf_pv_foot_L',
'FootPin.L',
'ToePin.L',
'cf_pv_foot_R',
'FootPin.R',
'ToePin.R',
]
armature = c.get_armature()
def set_armature_layer(bone_name, show_layer, hidden = False):
'''Assigns a bone to a bone collection.'''
bone = armature.data.bones.get(bone_name)
if bone:
if bpy.app.version[0] == 3:
armature.data.bones[bone_name].layers = (
True, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False,
False, False, False, False, False, False, False, False
)
#have to show the bone on both layer 1 and chosen layer before setting it to just chosen layer
armature.data.bones[bone_name].layers[show_layer] = True
armature.data.bones[bone_name].layers[0] = False
armature.data.bones[bone_name].hide = hidden
else:
show_layer = str(show_layer)
bone.collections.clear()
if armature.data.bones.get(bone_name):
if armature.data.collections.get(show_layer):
armature.data.collections[show_layer].assign(armature.data.bones.get(bone_name))
else:
armature.data.collections.new(show_layer)
armature.data.collections[show_layer].assign(armature.data.bones.get(bone_name))
armature.data.bones[bone_name].hide = hidden
c.switch(armature, 'OBJECT')
for bone in layer0_bones:
set_armature_layer(bone, 0)
for bone in layer1_bones:
set_armature_layer(bone, 1)
if not bpy.context.scene.kkbp.armature_dropdown == 'B':
return
c.kklog('Running Rigify conversion scripts...')
c.switch(armature, 'object')
try:
bpy.ops.kkbp.rigbefore('INVOKE_DEFAULT')
#remove the left ankle and right ankle's super copy prop
if bpy.app.version[0] != 3:
armature.pose.bones['Left ankle'].rigify_type = ""
armature.pose.bones['Right ankle'].rigify_type = ""
except:
if 'Calling operator "bpy.ops.pose.rigify_layer_init" error, could not be found' in traceback.format_exc():
c.kklog("There was an issue preparing the rigify metarig. \nMake sure the Rigify addon is installed and enabled. Skipping operation...", 'error')
c.kklog(traceback.format_exc())
return
bpy.ops.pose.rigify_generate()
bpy.ops.kkbp.rigafter('INVOKE_DEFAULT')
#make sure the new bones on the generated rig retain the KKBP outfit id entry
rig = bpy.context.active_object
rig['rig'] = True
rig['name'] = c.get_name()
#Take the IDs from all org bones and copy them over to the generated / helper bones
for bone in rig.data.bones:
if bone.get('id') and bone.name.startswith('ORG-'):
bone_base_name = bone.name[4:] # Remove 'ORG-' prefix
for bone_name in [
bone_base_name,
'DEF-' + bone_base_name,
bone_base_name + '_ik',
bone_base_name + '_ik.parent',
bone_base_name + '_master',
'MCH-' + bone_base_name,
'MCH-' + bone_base_name + '_drv',
]:
if rig.data.bones.get(bone_name):
rig.data.bones[bone_name]['id'] = bone['id']
armature.hide_set(True)
bpy.ops.object.select_all(action='DESELECT')
#make sure everything is deselected in edit mode for the body
body = c.get_body()
bpy.ops.object.select_all(action='DESELECT')
body.select_set(True)
bpy.context.view_layer.objects.active=body
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.ops.object.select_all(action='DESELECT')
rig.select_set(True)
bpy.context.view_layer.objects.active=rig
rig.show_in_front = True
bpy.context.scene.tool_settings.transform_pivot_point = 'INDIVIDUAL_ORIGINS'
bpy.context.tool_settings.mesh_select_mode = (False, False, True) #enable face select in edit mode
return {'FINISHED'}
def apply_sfw(self):
if not bpy.context.scene.kkbp.sfw_mode:
return
c.kklog('Applying mesh adjustments...')
#mark nsfw parts of mesh as freestyle faces so they don't show up in the outline
body = c.get_body()
c.switch(body, mode = 'EDIT')
def mark_group_as_freestyle(group_list):
for group in group_list:
group_found = body.vertex_groups.find(group)
if group_found > -1:
bpy.context.object.active_material_index = group_found
bpy.ops.object.vertex_group_select()
# else:
# c.kklog('Group wasn\'t found when freestyling vertex groups: ' + group, 'warn')
bpy.ops.mesh.mark_freestyle_face(clear=False)
freestyle_list = [
'cf_j_bnip02_L', 'cf_j_bnip02_R',
'cf_s_bust03_L', 'cf_s_bust03_R']
mark_group_as_freestyle(freestyle_list)
bpy.ops.mesh.select_all(action = 'DESELECT')
#delete nsfw parts of the mesh
def delete_group_and_bone(ob, group_list):
c.switch(ob, 'EDIT')
bpy.ops.mesh.select_all(action = 'DESELECT')
for group in group_list:
group_found = ob.vertex_groups.find(group)
if group_found > -1:
bpy.context.object.vertex_groups.active_index = group_found
bpy.ops.object.vertex_group_select()
# else:
# c.kklog('Group wasn\'t found when deleting vertex groups: ' + group, 'warn')
bpy.ops.mesh.delete(type='VERT')
bpy.ops.mesh.select_all(action = 'DESELECT')
delete_list = ['cf_s_bnip025_L', 'cf_s_bnip025_R', 'cf_s_bnip02_L', 'cf_s_bnip02_R',
'cf_j_kokan', 'cf_j_ana', 'cf_d_ana', 'cf_d_kokan', 'cf_s_ana',
'Vagina_Root', 'Vagina_B', 'Vagina_F', 'Vagina_001_L', 'Vagina_002_L',
'Vagina_003_L', 'Vagina_004_L', 'Vagina_005_L', 'Vagina_001_R', 'Vagina_002_R',
'Vagina_003_R', 'Vagina_004_R', 'Vagina_005_R']
delete_group_and_bone(body, delete_list)
#also do this on the clothes because the bra can show up
delete_list = ['cf_s_bnip02_L', 'cf_s_bnip02_R', 'cf_s_bnip025_L', 'cf_s_bnip025_R', ]
for ob in [o for o in bpy.data.objects if o.get('KKBP tag') == 'outfit']:
delete_group_and_bone(ob, delete_list)
#force the sfw alpha mask on the body
for mat_prefix in ['KK Body', 'Outline Body']:
body_mat = body.material_slots[mat_prefix + ' ' + c.get_name()].material
body_mat.node_tree.nodes["combine"].inputs['Force custom mask'].default_value = 1
new_group = body_mat.node_tree.nodes['combine'].node_tree.copy()
body_mat.node_tree.nodes['combine'].node_tree = new_group
if bpy.app.version[0] == 3:
new_group.inputs['Force custom mask'].hide_value = True
else:
new_group.interface.items_tree['Force custom mask'].hide_value = True
#get rid of the nsfw groups on the body
body_mat = body.material_slots['KK Body ' + c.get_name()].material
body_mat.node_tree.nodes.remove(body_mat.node_tree.nodes['texturesnsfw'])
for nono in [
'Nipple',
'Nipple (alpha)',
'Genital',
'Underhair',
'Genital intensity',
'Genital saturation',
'Genital hue',
'Underhair color',
'Underhair intensity',
'Nipple base',
'Nipple base 2',
'Nipple shine',
'Nipple rim']:
if bpy.app.version[0] == 3:
body_mat.node_tree.nodes['light'].node_tree.inputs.remove(body_mat.node_tree.nodes['light'].node_tree.inputs[nono])
else:
body_mat.node_tree.nodes['light'].node_tree.interface.remove(body_mat.node_tree.nodes['light'].node_tree.interface.items_tree[nono])
#delete nsfw bones if sfw mode enebled
rig = c.get_rig()
if bpy.context.scene.kkbp.sfw_mode and bpy.context.scene.kkbp.armature_dropdown == 'B':
if bpy.app.version[0] != 3:
rig.data.collections_all['29'].is_visible = True
def delete_bone(group_list):
#delete bones too
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.ops.object.select_all(action='DESELECT')
rig.select_set(True)
bpy.context.view_layer.objects.active = rig
bpy.ops.object.mode_set(mode = 'EDIT')
bpy.ops.armature.select_all(action='DESELECT')
for bone in group_list:
if rig.data.bones.get(bone):
rig.data.edit_bones[bone].select = True
bpy.ops.kkbp.cats_merge_weights()
# else:
# c.kklog('Bone wasn\'t found when deleting bones: ' + bone, 'warn')
bpy.ops.armature.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode = 'OBJECT')
delete_list = ['cf_s_bnip025_L', 'cf_s_bnip025_R',
'cf_j_kokan', 'cf_j_ana', 'cf_d_ana', 'cf_d_kokan', 'cf_s_ana',
'cf_J_Vagina_root',
'cf_J_Vagina_B',
'cf_J_Vagina_F',
'cf_J_Vagina_L.005',
'cf_J_Vagina_R.005',
'cf_J_Vagina_L.004',
'cf_J_Vagina_L.001',
'cf_J_Vagina_L.002',
'cf_J_Vagina_L.003',
'cf_J_Vagina_R.001',
'cf_J_Vagina_R.002',
'cf_J_Vagina_R.003',
'cf_J_Vagina_R.004',
'cf_j_bnip02root_L',
'cf_j_bnip02_L',
'cf_s_bnip01_L',
#'cf_s_bust03_L',
'cf_s_bust02_L',
'cf_j_bnip02root_R',
'cf_j_bnip02_R',
'cf_s_bnip01_R',
#'cf_s_bust03_R',
'cf_s_bust02_R',]
delete_bone(delete_list)
if bpy.app.version[0] != 3:
rig.data.collections_all['29'].is_visible = False
def separate_meshes(self):
if bpy.context.scene.kkbp.categorize_dropdown == 'B':
#separate each outfit by material
for obj in c.get_outfits():
if obj.modifiers.get('Outline Modifier'):
obj.modifiers['Outline Modifier'].show_render = False
obj.modifiers['Outline Modifier'].show_viewport = False
c.switch(obj, 'OBJECT')
bpy.ops.object.material_slot_remove_unused()
c.switch(obj, 'EDIT')
bpy.ops.mesh.separate(type='MATERIAL')
#once they are all separated, rename them to their material name
for obj in c.get_outfits():
try:
obj.name = obj.material_slots[0].name
except:
#oh well
pass