''' This file performs the following operations . Separates the rigged tongue, hair, shift/hang state clothing, hitboxes, and puts them into their own collections · Delete mask material, shadowcast mesh, and bonelyfans mesh if present · Remove shapekeys on all objects except body / tears / gag eyes · Rename UV maps on body object and outfit objects · Translates all shapekey names to english · Combines shapekeys based on face part prefix and emotion suffix · Creates tear shapekeys · Creates gag eye shapekeys and drivers for shapekeys . Removes doubles on body object to prevent seams (if selected) · Mark certain body materials as freestyle faces for freestyle exclusion ''' import re import bpy from .. import common as c from ..extras.linkshapekeys import link_keys class modify_mesh(bpy.types.Operator): bl_idname = "kkbp.modifymesh" bl_label = bl_idname bl_description = bl_idname bl_options = {'REGISTER', 'UNDO'} def execute(self, context): try: self.rename_uv_maps() self.clean_up_duplicates() self.separate_rigged_tongue() self.separate_hair() self.separate_alternate_clothing() self.delete_shad_bone() self.separate_hitboxes() self.delete_mask_quad() self.remove_unused_shapekeys() self.translate_shapekeys() self.combine_shapekeys() self.create_tear_shapekeys() self.create_gag_eye_shapekeys() self.remove_body_seams() self.mark_body_freestyle_faces() c.clean_orphaned_data() return {'FINISHED'} except Exception as error: c.handle_error(self, error) return {"CANCELLED"} def clean_up_duplicates(self): '''Removes duplicate materials on the body object (there should only be one each)''' c.clean_orphaned_data() pattern = re.compile(r'\.\d{3}$') for material in c.get_body().data.materials: if pattern.search(material.name): new_name = material.name[:-4] c.kklog(f'Renamed duplicate body material {material.name} to {new_name}') material.name = new_name material['id'] = new_name material['name'] = new_name # %% Main functions def separate_rigged_tongue(self): """ Separates the rigged tongue object from the main body mesh. If no rigged tongue, create one if general tongue exists """ rigged_tongue_material = None general_tongue_material = None tongue_datas = c.json_file_manager.get_material_info_by_smr('o_tang') if tongue_datas is None: c.kklog('No tongue', 'warn') c.print_timer('Skipped') return for item in tongue_datas: if item['SMRPath'].endswith('N_cf_haed/o_tang'): general_tongue_material = item['MaterialInformation'][0]['MaterialName'] else: rigged_tongue_material = item['MaterialInformation'][0]['MaterialName'] if rigged_tongue_material == general_tongue_material: rigged_tongue_material += '.001' # if rigged tongue doesn't exist, # rename tongue material to .001, duplicate tongue material and rename to general name, separate mesh by .001, # duplicate tongue mesh as general tongue and join back to body if rigged_tongue_material is None or general_tongue_material is None: # Some model only have N_cf_haed/o_tang or n_tang/o_tang if general_tongue_material: base_name = general_tongue_material else: base_name = rigged_tongue_material general_tongue_material = base_name rigged_tongue_material = base_name + '.001' ori_material = bpy.data.materials[general_tongue_material] # rename original material to .001, so we do not need to change the faces' s material to new one ori_material['name'] = rigged_tongue_material ori_material['id'] = rigged_tongue_material ori_material.name = rigged_tongue_material new_material = ori_material.copy() new_material['name'] = general_tongue_material new_material['id'] = general_tongue_material new_material.name = general_tongue_material tongue = self.separate_materials(c.get_body(), [rigged_tongue_material], 'Tongue (rigged) ' + c.get_name()) c.get_body().data.materials.append(new_material) bpy.ops.object.material_slot_move(direction='DOWN') # copy the tongue mesh and join it back to body bpy.ops.object.mode_set(mode='OBJECT') tongue_copy = tongue.copy() tongue_copy.data = tongue.data.copy() bpy.context.collection.objects.link(tongue_copy) bpy.ops.object.select_all(action='DESELECT') tongue_copy.select_set(True) c.get_body().select_set(True) bpy.context.view_layer.objects.active = c.get_body() bpy.ops.object.join() tongue['tongue'] = True else: tongue = self.separate_materials(c.get_body(), [rigged_tongue_material], 'Tongue (rigged) ' + c.get_name()) tongue['tongue'] = True # Now remap the rigged tongue material with the original to allow the rigged tongue and the tongue on the body to share the same material if bpy.data.materials.get(rigged_tongue_material): bpy.data.materials[rigged_tongue_material].user_remap( bpy.data.materials[general_tongue_material]) bpy.data.materials.remove(bpy.data.materials[rigged_tongue_material],do_unlink=True) # forcing to delete c.print_timer('separate_rigged_tongue') def separate_hair(self): '''Separates the hair from the clothes object''' outfits = c.get_outfits() #Separate the hair from each outfit # material_data = c.json_file_manager.get_json_file('KK_MaterialDataComplete.json') material_data = c.json_file_manager.get_materials_info() hair_materials = [ material['MaterialName'] for obj in material_data.values() for sub_obj in obj for material in sub_obj['MaterialInformation'] if material['isHair'] ] for outfit in outfits: #find all the hair mats for this outfit cur_hair_mat_list = [] outfit_materials = [mat_slot.material.name for mat_slot in outfit.material_slots] for material in hair_materials: # some hair materials are repeated. The order goes 'hair_material', 'hair_material 00', 'hair_material 01', etc. Check for those too. cur_hair_mat_list.extend([m for m in outfit_materials if material in m]) if cur_hair_mat_list: hair_object = self.separate_materials(outfit, cur_hair_mat_list, 'Hair ' + outfit.name) hair_object['hair'] = True hair_object['outfit'] = False c.print_timer('separate_hair') def separate_alternate_clothing(self): '''Separates the alternate clothing pieces then hides them''' #These are the enum indexes that need to be separated clothes_labels = { 999: 'Indoor shoes', 93: 'Top shift', 97: 'Top shift', 112: 'Top shift', 114: 'Top shift', 116: 'Top shift', 120: 'Top shift', 95: 'Bottom shift', 99: 'Bottom shift', 101: 'Bra shift', 118: 'Bra shift', 107: 'Underwear shift', 108: 'Underwear hang', 110: 'Pantyhose shift', } material_data = c.json_file_manager.get_materials_info() for outfit in c.get_outfits(): for label in clothes_labels: materials_to_separate = [] for smr_name, smr_items in material_data.items(): for smr_item in smr_items: if label == smr_item['EnumIndex']: materials_to_separate.extend(c.get_material_names(smr_name)) if materials_to_separate: alt_clothes = self.separate_materials(outfit, materials_to_separate, clothes_labels[label] + ' ' + outfit['id'] + ' ' + c.get_name()) if alt_clothes: alt_clothes['alt'] = True alt_clothes['outfit'] = False c.kklog('Separated {} alternate clothing {} automatically'.format(materials_to_separate, clothes_labels[label])) c.print_timer('separate_alternate_clothing') def delete_shad_bone(self): '''Delete the shadowcast and bonelyfans meshes, if present''' mat_list = ['c_m_shadowcast', 'Standard'] shadowcast = self.separate_materials(c.get_body(), mat_list, 'shadowcast', search_type = 'fuzzy') if shadowcast: bpy.data.objects.remove(shadowcast) #Delete the bonelyfans mesh if any # mat_list = ['Bonelyfans', 'Bonelyfans.001'] # some model have .002, even .003, .004 mat_list = c.get_material_names('Highlight_o_body_a_rend') mat_list.extend(c.get_material_names('Highlight_cf_O_face_rend')) mat_list = list(set(mat_list)) extended = [] for mat in mat_list: index = 1 while bpy.data.materials.get((name := f'{mat}.{index:03d}')): index += 1 extended.append(name) mat_list.extend(extended) bonely = self.separate_materials(c.get_body(), mat_list, 'bonelyfans') if bonely: bpy.data.objects.remove(bonely) c.print_timer('delete_shad_bone') def separate_hitboxes(self): '''Separate the hitbox mesh, if present''' material_data = c.json_file_manager.get_materials_info() hitbox_names = [] for smr_name, smr_infos in material_data.items(): if smr_name.startswith('o_hit'): hitbox_names.extend([ item['MaterialName'] for smr_info in smr_infos for item in smr_info['MaterialInformation'] ]) hitbox_names = list(set(hitbox_names)) # first remap all of the duplicate hitbox materials to share the same material name, or some separations will be missed for hitbox_name in hitbox_names: index = 1 while bpy.data.materials.get((hitbox := f'{hitbox_name}.{index:03d}')): bpy.data.materials[hitbox].user_remap(bpy.data.materials[hitbox_name]) bpy.data.materials.remove(bpy.data.materials[hitbox]) index += 1 hitbox = self.separate_materials(c.get_body(), hitbox_names, 'Hitboxes Body ' + c.get_name()) if hitbox: hitbox['hitbox'] = True hitbox['body'] = False for outfit in c.get_outfits(): hitbox = self.separate_materials(outfit, hitbox_names, 'Hitboxes ' + outfit['id'] + ' ' + c.get_name()) if hitbox: hitbox['hitbox'] = True hitbox['outfit'] = False c.move_and_hide_collection(c.get_hitboxes(), "Hitboxes " + c.get_name()) c.print_timer('separate_hitboxes') def delete_mask_quad(self): '''delete the mask material if not in smr mode''' material_names = [] material_data = c.json_file_manager.get_materials_info() for smr_name, smr_infos in material_data.items(): if smr_name.startswith('o_Mask'): material_names.extend([ item['MaterialName'] for smr_info in smr_infos for item in smr_info['MaterialInformation'] if item['ShaderName'] == "Shader Forge/AlphaMaskMultiply" ]) material_names = set(material_names) for outfit in c.get_outfits(): for mat in outfit.material_slots: if mat.name in material_names: self.delete_materials(outfit, [mat]) c.print_timer('delete_mask_quad') def remove_unused_shapekeys(self): '''remove shapekeys on all hair and clothes objects''' if bpy.context.scene.kkbp.shapekeys_dropdown not in ['A', 'B']: return object_list = c.get_outfits() object_list.extend(c.get_alts()) object_list.extend(c.get_hairs()) object_list.extend(c.get_hitboxes()) object_list = [o for o in object_list if o.data.shape_keys] for obj in object_list: for key in obj.data.shape_keys.key_blocks.keys(): obj.shape_key_remove(obj.data.shape_keys.key_blocks[key]) c.print_timer('remove_unused_shapekeys') def rename_uv_maps(self): #Make UV map names clearer c.get_body().data.uv_layers[0].name = 'uv_main' c.get_body().data.uv_layers[1].name = 'uv_nipple_and_shine' c.get_body().data.uv_layers[2].name = 'uv_underhair' c.get_body().data.uv_layers[3].name = 'uv_eyeshadow' for outfit in c.get_outfits(): outfit.data.uv_layers[0].name = 'uv_main' outfit.data.uv_layers[1].name = 'uv_nipple_and_shine' outfit.data.uv_layers[2].name = 'uv_underhair' outfit.data.uv_layers[3].name = 'uv_eyeshadow' c.print_timer('rename_uv_maps') def translate_shapekeys(self): '''Renames the face shapekeys to english''' if not bpy.context.scene.kkbp.shapekeys_dropdown in ['A', 'B']: return translation_dict = { #Prefixes "eye_face.f00": "Eyes", "kuti_face.f00": "Lips", "eye_siroL.sL00": "EyeWhitesL", "eye_siroR.sR00": "EyeWhitesR", "eye_line_u.elu00": "Eyelashes1", "eye_line_l.ell00": "Eyelashes2", "eye_naM.naM00": "EyelashesPos", "eye_nose.nl00": "NoseTop", "kuti_nose.nl00": "NoseBot", "kuti_ha.ha00": "Teeth", "kuti_yaeba.y00": "Fangs", "kuti_sita.t00": "Tongue", "mayuge.mayu00": "KK Eyebrows", "eye_naL.naL00": "Tear_big", "eye_naM.naM00": "Tear_med", "eye_naS.naS00": "Tear_small", #Prefixes (Yelan headmod exception) "namida_l": "Tear_big", "namida_m": "Tear_med", "namida_s": "Tear_small", 'tang.': 'Tongue', #Emotions (eyes and mouth) "_def_": "_default_", "_egao_": "_smile_", "_bisyou_": "_smile_sharp_", "_uresi_ss_": "_happy_slight_", "_uresi_s_": "_happy_moderate_", "_uresi_": "_happy_broad_", "_doki_ss_": "_doki_slight_", "_doki_s_": "_doki_moderate_", "_ikari_": "_angry_", "_ikari02_": "_angry_2_", "_sinken_": "_serious_", "_sinken02_": "_serious_1_", "_sinken03_": "_serious_2_", "_keno_": "_hate_", "_sabisi_": "_lonely_", "_aseri_": "_impatient_", "_huan_": "_displeased_", "_human_": "_displeased_", "_akire_": "_amazed_", "_odoro_": "_shocked_", "_odoro_s_": "_shocked_moderate_", "_doya_": "_smug_", "_pero_": "_lick_", "_name_": "_eating_", "_tabe_": "_eating_2_", "_kuwae_": "_hold_in_mouth_", "_kisu_": "_kiss_", "_name02_": "_tongue_out_", "_mogu_": "_chewing_", "_niko_": "_cartoon_mouth_", "_san_": "_triangle_", #Emotions (Eyes) "_winkl_": "_wink_left_", "_winkr_": "_wink_right_", "_setunai_": "_distress_", "_tere_": "_shy_", "_tmara_": "_bored_", "_tumara_": "_bored_", "_kurusi_": "_pain_", "_sian_": "_thinking_", "_kanasi_": "_sad_", "_naki_": "_crying_", "_rakutan_": "_dejected_", "_komaru_": "_worried_", "_gag": "_gageye", "_gyul_": "_squeeze_left_", "_gyur_": "_squeeze_right_", "_gyu_": "_squeeze_", "_gyul02_": "_squeeze_left_2_", "_gyur02_": "_squeeze_right_2_", "_gyu02_": "_squeeze_2_", #Emotions (Eyebrows) "_koma_": "_worried_", "_gimoL_": "_doubt_left_", "_gimoR_": "_doubt_right_", "_sianL_": "_thinking_left_", "_sianR_": "_thinking_right_", "_oko_": "_angry_", "_oko2L_": "_angry_left_", "_oko2R_": "_angry_right_", #Emotions extra "_s_": "_small_", "_l_": "_big_", #Emotions Yelan headmod exception 'T_Default': '_default_op', } c.get_body().active_shape_key_index = 0 originalExists = False for shapekey in bpy.data.shape_keys: for keyblock in shapekey.key_blocks: #check if the original shapekeys still exists if 'Basis' not in keyblock.name: if 'Lips' in keyblock.name: originalExists = True #rename original shapekeys for shapekey in bpy.data.shape_keys: for keyblock in shapekey.key_blocks: for key in translation_dict: if 'gageye' not in keyblock.name: keyblock.name = keyblock.name.replace(key, translation_dict[key]) try: #delete the KK shapekeys if the original shapekeys still exist if originalExists and 'KK ' in keyblock.name and 'KK Eyebrows' not in keyblock.name: c.get_body().active_shape_key_index = c.get_body().data.shape_keys.key_blocks.keys().index(keyblock.name) bpy.ops.object.shape_key_remove() #only way to do this is with ops? except: #or not c.kklog("Couldn't delete shapekey: " + keyblock.name, 'error') pass c.print_timer('translate_shapekeys') def combine_shapekeys(self): '''Creates new, full shapekeys using the existing partial shapekeys, and deletes the partial shapekeys if user didn't elect to keep them in the panel''' if not bpy.context.scene.kkbp.shapekeys_dropdown in ['A', 'B']: return #make the basis shapekey active c.switch(c.get_body(), 'object') c.get_body().active_shape_key_index = 0 def whatCat(keyName): #Eyelashes1 is used because I couldn't see a difference between the other one and they overlap if both are used #EyelashPos is unused because Eyelashes work better and it overlaps with Eyelashes eyes = [keyName.find("Eyes"), keyName.find("NoseT"), keyName.find("Eyelashes1"), keyName.find("EyeWhites"), keyName.find('Tear_big'), keyName.find('Tear_med'), keyName.find('Tear_small')] if not all(v == -1 for v in eyes): return 'Eyes' mouth = [keyName.find("NoseB"), keyName.find("Lips"), keyName.find("Tongue"), keyName.find("Teeth"), keyName.find("Fangs")] if not all(v==-1 for v in mouth): return 'Mouth' return 'None' #setup two arrays to keep track of the shapekeys that have been used #and the shapekeys currently in use used = [] inUse = [] #These mouth shapekeys require the default teeth and tongue shapekeys to be active correctionList = ['_u_small_op', '_u_big_op', '_e_big_op', '_o_small_op', '_o_big_op', '_neko_op', '_triangle_op'] shapekey_block = bpy.data.shape_keys[c.get_body().data.shape_keys.name].key_blocks ACTIVE = 0.9 def activate_shapekey(key_act): if shapekey_block.get(key_act) != None: shapekey_block[key_act].value = ACTIVE #go through the keyblock list twice #Do eye shapekeys first then mouth shapekeys for type in ['Eyes_', 'Lips_']: counter = len(shapekey_block) for current_keyblock in shapekey_block: counter = counter - 1 if (counter == 0): break #categorize the shapekey (eye or mouth) cat = whatCat(current_keyblock.name) #get the emotion from the shapekey name if (cat != 'None') and ('KK' not in current_keyblock.name) and (type in current_keyblock.name): emotion = current_keyblock.name[current_keyblock.name.find("_"):] #go through every shapekey to check if any match the current shapekey's emotion for supporting_shapekey in shapekey_block: #If the's emotion matches the current one and is the correct category... if emotion in supporting_shapekey.name and cat == whatCat(supporting_shapekey.name): #and this key has hasn't been used yet activate it, else skip to the next if (supporting_shapekey.name not in used): supporting_shapekey.value = ACTIVE inUse.append(supporting_shapekey.name) #The shapekeys for the current emotion are now all active #Some need manual corrections correction_needed = False for cor in correctionList: if cor in current_keyblock.name: correction_needed = True if correction_needed: activate_shapekey('Fangs_default_op') activate_shapekey('Teeth_default_op') activate_shapekey('Tongue_default_op') if ('_e_small_op' in current_keyblock.name): activate_shapekey('Fangs_default_op') activate_shapekey('Lips_e_small_op') if ('_cartoon_mouth_op' in current_keyblock.name): activate_shapekey('Tongue_default_op') activate_shapekey('Lips_cartoon_mouth_op') if ('_smile_sharp_op' in current_keyblock.name and cat == 'Mouth'): if shapekey_block.get('Teeth_smile_sharp_op1') != None: shapekey_block['Teeth_smile_sharp_op1'].value = 0 activate_shapekey('Lips_smile_sharp_op') if ('_eating_2_op' in current_keyblock.name): activate_shapekey('Fangs_default_op') activate_shapekey('Teeth_tongue_out_op') activate_shapekey('Tongue_serious_2_op') activate_shapekey('Lips_eating_2_op') if ('_i_big_op' in current_keyblock.name): activate_shapekey('Teeth_i_big_cl') activate_shapekey('Fangs_default_op') activate_shapekey('Lips_i_big_op') if ('_i_small_op' in current_keyblock.name): activate_shapekey('Teeth_i_small_cl') activate_shapekey('Fangs_default_op') activate_shapekey('Lips_i_small_op') if (current_keyblock.name not in used): c.get_body().shape_key_add(name=('KK ' + cat + emotion)) #make sure this shapekey set isn't used again used.extend(inUse) inUse =[] #reset all shapekey values for reset_keyblock in shapekey_block: reset_keyblock.value = 0 #lazy crash prevention if counter % 20 == 0: bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1) #Delete all shapekeys that don't have a "KK" in their name #Don't delete the Basis shapekey though #If no KK shapekeys were generated, something went wrong so don't delete any shapekeys keep_partial_shapekeys = bpy.context.scene.kkbp.shapekeys_dropdown == 'B' it_worked = True if [key for key in shapekey_block if 'KK ' in key.name] else False if it_worked and not keep_partial_shapekeys: for remove_shapekey in shapekey_block: try: if ('KK ' not in remove_shapekey.name and remove_shapekey.name != shapekey_block[0].name): c.get_body().shape_key_remove(remove_shapekey) except: c.kklog('Couldn\'t remove shapekey ' + remove_shapekey.name, 'error') pass else: c.kklog('Original shapekeys were not deleted', 'warn') #make the basis shapekey active c.get_body().active_shape_key_index = 0 #and reset the pivot point to median bpy.context.scene.tool_settings.transform_pivot_point = 'MEDIAN_POINT' c.print_timer('combine_shapekeys') def create_tear_shapekeys(self): '''Separate tears from body and create tear shapekeys''' if bpy.context.scene.kkbp.shapekeys_dropdown not in ['A', 'B']: return # check if the tear material even exists try: tear_material_name = c.get_material_names('cf_O_namida_L')[0] except: c.kklog('Tear material did not exist.', 'warn') return # Create a reverse shapekey for each tear material c.switch(c.get_body(), 'edit') # Move tears and gag backwards on the basis shapekey # use head mesh as reference location face_material = c.get_material_names('cf_O_face') if face_material: bpy.context.object.active_material_index = c.get_body().data.materials.find(face_material[0]) bpy.ops.object.material_slot_select() # refresh selection, then get head location bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='EDIT') selected_verts = [v.co.y for v in c.get_body().data.vertices if v.select] loc = 0 for y in selected_verts: loc += y middle_of_head = loc / len(selected_verts) c.switch(c.get_body(), 'edit') tear_mats = { 'cf_O_namida_L': ("Tears big", []), 'cf_O_namida_M': ("Tears med", []), 'cf_O_namida_S': ('Tears small', []), 'cf_O_gag_eye_00': ("Gag eye 00", []), 'cf_O_gag_eye_01': ("Gag eye 01", []), 'cf_O_gag_eye_02': ("Gag eye 02", []), } for cat, cat_data in tear_mats.items(): mats = c.get_material_names(cat) if (m_flag := ('M' in cat)) or 'S' in cat: mats = [m + ('.001' if m_flag else '.002') for m in mats] # tears share a material name, so add a .001 for mat in mats: bpy.context.object.active_material_index = c.get_body().data.materials.find(mat) bpy.ops.object.material_slot_select() cat_data[1].extend(mats) # refresh selection, then move tears a random amount backwards bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='EDIT') selected_verts = [v for v in c.get_body().data.vertices if v.select] amount_to_move_tears_back = 2 * (selected_verts[0].co.y - middle_of_head) bpy.ops.transform.translate(value=(0, abs(amount_to_move_tears_back), 0)) # move the tears forwards again the same amount in individual new shapekeys for cat, cat_data in tear_mats.items(): for mat in cat_data[1]: c.switch(c.get_body(), 'object') bpy.ops.object.shape_key_add(from_mix=False) c.get_body().data.shape_keys.key_blocks[-1].name = cat_data[0] last_shapekey = len(c.get_body().data.shape_keys.key_blocks) - 1 bpy.context.object.active_shape_key_index = last_shapekey c.switch(c.get_body(), 'edit') bpy.context.object.active_material_index = c.get_body().data.materials.find(mat) if c.get_body().data.materials.find(mat) == -1: bpy.context.object.active_material_index += 1 else: bpy.context.object.active_material_index = c.get_body().data.materials.find(mat) bpy.ops.object.material_slot_select() # find a random vertex location of the tear and move it forwards c.switch(c.get_body(), 'object') bpy.ops.object.mode_set(mode='EDIT') bpy.ops.transform.translate(value=(0, -1 * abs(amount_to_move_tears_back), 0)) c.switch(c.get_body(), 'object') bpy.ops.object.shape_key_move(type='TOP' if tear_material_name in mat else 'BOTTOM') # Move the Eye, eyewhite and eyeline materials back on the KK gageye shapekey bpy.context.object.active_shape_key_index = bpy.context.object.data.shape_keys.key_blocks.find('KK Eyes_gageye') c.switch(c.get_body(), 'edit') for cat in [ 'cf_Ohitomi_L', 'cf_Ohitomi_R', 'cf_Ohitomi_L02', 'cf_Ohitomi_R02', 'cf_O_eyeline', 'cf_O_eyeline_low']: mats = c.get_material_names(cat) # also append the duplicated eyewhite material mats.append('cf_m_sirome_00.001') for mat in mats: bpy.context.object.active_material_index = c.get_body().data.materials.find(mat) bpy.ops.object.material_slot_select() # find a random vertex location of the eye and move it backwards c.switch(c.get_body(), 'object') bpy.ops.object.mode_set(mode='EDIT') bpy.ops.transform.translate(value=(0, 2.5 * abs(amount_to_move_tears_back), 0)) c.switch(c.get_body(), 'object') # Merge the tear materials c.switch(c.get_body(), 'edit') to_merge_materials = tear_mats['cf_O_namida_L'][1] to_merge_materials.extend(tear_mats['cf_O_namida_M'][1]) to_merge_materials.extend(tear_mats['cf_O_namida_S'][1]) for mat in to_merge_materials: bpy.context.object.active_material_index = c.get_body().data.materials.find(mat) bpy.ops.object.material_slot_select() bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name) bpy.ops.object.material_slot_assign() bpy.ops.mesh.select_all(action='DESELECT') # make a vertex group that does not contain the tears bpy.ops.object.vertex_group_add() bpy.ops.mesh.select_all(action='SELECT') c.get_body().vertex_groups.active.name = "Body without Tears" bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name) bpy.ops.object.material_slot_deselect() bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name + '.001') bpy.ops.object.material_slot_deselect() bpy.context.object.active_material_index = c.get_body().data.materials.find(tear_material_name + '.002') bpy.ops.object.material_slot_deselect() bpy.ops.object.vertex_group_assign() # Separate tears from body object # link shapekeys of tears to body tears = self.separate_materials(c.get_body(), to_merge_materials, 'Tears ' + c.get_name()) tears['tears'] = True bpy.ops.object.mode_set(mode='OBJECT') link_keys(c.get_body(), [tears]) c.print_timer('create_tear_shapekeys') def create_gag_eye_shapekeys(self): '''Separate gag eyes from body and create gag eye shapekeys''' if bpy.context.scene.kkbp.shapekeys_dropdown not in ['A', 'B'] or len(c.get_material_names('cf_O_gag_eye_00')) == 0: return bpy.context.view_layer.objects.active=c.get_body() gag_keys = [ 'Circle Eyes 1', 'Circle Eyes 2', 'Spiral Eyes', 'Heart Eyes', 'Fiery Eyes', 'Cartoony Wink', 'Vertical Line', 'Cartoony Closed', 'Horizontal Line', 'Cartoony Crying' ] for key in gag_keys: bpy.ops.object.mode_set(mode = 'OBJECT') bpy.ops.object.shape_key_add(from_mix=False) last_shapekey = len(c.get_body().data.shape_keys.key_blocks)-1 c.get_body().data.shape_keys.key_blocks[-1].name = key bpy.context.object.active_shape_key_index = last_shapekey bpy.ops.object.shape_key_move(type='TOP') def create_gag_eye_driver(keyblock: str, condition: str): '''creates a gag eye driver''' skey_driver = bpy.data.shape_keys[0].key_blocks[keyblock].driver_add('value') skey_driver.driver.type = 'SCRIPTED' for key in gag_keys: newVar = skey_driver.driver.variables.new() newVar.name = key.replace(' ','') newVar.type = 'SINGLE_PROP' newVar.targets[0].id_type = 'KEY' newVar.targets[0].id = c.get_body().data.shape_keys newVar.targets[0].data_path = 'key_blocks["' + key + '"].value' skey_driver.driver.expression = condition bpy.context.object.active_shape_key_index = 0 #make most gag eye shapekeys activate the body's gag key if the KK gageeye shapekey was created if bpy.data.shape_keys[0].key_blocks.get('KK Eyes_gageye'): condition = [key.replace(' ', '') for key in gag_keys if 'Fiery' not in key] create_gag_eye_driver('KK Eyes_gageye', '1 if ' + ' or '.join(condition) + ' else 0' ) create_gag_eye_driver('Gag eye 00', '1 if CircleEyes1 or CircleEyes2 or VerticalLine or CartoonyClosed or HorizontalLine else 0' ) create_gag_eye_driver('Gag eye 01', '1 if HeartEyes or SpiralEyes else 0' ) create_gag_eye_driver('Gag eye 02', '1 if FieryEyes or CartoonyWink or CartoonyCrying else 0' ) #make a vertex group that does not contain the gag_eyes bpy.ops.object.vertex_group_add() c.switch(c.get_body(), 'edit') bpy.ops.mesh.select_all(action='SELECT') c.get_body().vertex_groups.active.name = "Body without Gag eyes" gag_eye_materials = [] gag_eye_data = c.json_file_manager.get_material_info_by_smr('cf_O_gag_eye_00') gag_eye_materials.extend([ item['MaterialName'] for smr_info in gag_eye_data for item in smr_info['MaterialInformation'] ]) gag_eye_data = c.json_file_manager.get_material_info_by_smr('cf_O_gag_eye_01') gag_eye_materials.extend([ item['MaterialName'] for smr_info in gag_eye_data for item in smr_info['MaterialInformation'] ]) gag_eye_data = c.json_file_manager.get_material_info_by_smr('cf_O_gag_eye_02') gag_eye_materials.extend([ item['MaterialName'] for smr_info in gag_eye_data for item in smr_info['MaterialInformation'] ]) gag_eye_materials = list(set(gag_eye_materials)) for material_name in gag_eye_materials: bpy.context.object.active_material_index = c.get_body().data.materials.find(material_name) bpy.ops.object.material_slot_deselect() bpy.ops.object.vertex_group_assign() # Separate gag from body object # link shapekeys of gag to body if gag_eye_materials: gag_eye = self.separate_materials(c.get_body(), gag_eye_materials, 'Gag Eyes ' + c.get_name()) gag_eye['gag'] = True gag_eye['body'] = False c.switch(c.get_body(), 'object') link_keys(c.get_body(), [gag_eye]) c.print_timer('create gag_eye_shapekeys') return c.print_timer('ignored gag_eye_shapekeys') def remove_body_seams(self): '''merge certain materials for the body object to prevent odd shading issues later on''' if not bpy.context.scene.kkbp.fix_seams: return c.switch(c.get_body(), 'edit') mats = c.get_material_names('cf_O_face') mats.extend(c.get_material_names('o_body_a')) bpy.context.tool_settings.mesh_select_mode = (True, False, False) #enable vertex select in edit mode for mat in mats: bpy.context.object.active_material_index = c.get_body().data.materials.find(mat) bpy.ops.object.material_slot_select() bpy.ops.mesh.remove_doubles(threshold=0.00001) # This operation still messes with the weights. # Maybe it's possible to save the 3D positions, weights, and UV positions for each duplicate vertex # then delete and make new vertices with saved info # The vertices on the body object seem to be consistent across imports according to https://github.com/FlailingFog/KK-Blender-Porter-Pack/issues/82 c.print_timer('remove_body_seams') def mark_body_freestyle_faces(self): c.switch(c.get_body(), 'edit') #mark certain materials as freestyle faces def mark_as_freestyle(mat_list: bpy.types.Material): for mat in mat_list: mat_found = c.get_body().data.materials.find(mat) if mat_found > -1: bpy.context.object.active_material_index = mat_found bpy.ops.object.material_slot_select() else: c.kklog('Material wasn\'t found when freestyling body materials: ' + mat, 'warn') bpy.ops.mesh.mark_freestyle_face(clear=False) freestyle_list = [ 'cf_Ohitomi_L02', 'cf_Ohitomi_R02', 'cf_Ohitomi_L', 'cf_Ohitomi_R', 'cf_O_eyeline_low', 'cf_O_eyeline', 'cf_O_noseline', 'cf_O_mayuge',] mats = [] for mat in freestyle_list: mats.extend(c.get_material_names(mat)) mark_as_freestyle(mats) bpy.ops.mesh.select_all(action = 'DESELECT') bpy.ops.object.mode_set(mode = 'OBJECT') c.print_timer('mark_body_freestyle_faces') def separate_materials(self, object: bpy.types.Object, mat_list: list[bpy.types.Material], new_object_name: str, search_type = 'exact') -> bpy.types.Object: '''Separates the materials in the mat_list on object, and renames the separated object to "new_object_name". Returns the separated object, or None if there was an error''' c.switch(object, 'edit') for mat in mat_list: mat_found = -1 if search_type == 'fuzzy' and ('cm_m_' in mat or 'c_m_' in mat or 'o_hit_' in mat or mat == 'cf_O_face_atari_M'): for matindex in range(0, len(object.data.materials), 1): if mat in object.data.materials[matindex].name: mat_found = matindex else: mat_found = object.data.materials.find(mat) if mat_found > -1: bpy.context.object.active_material_index = mat_found #moves the materials in a specific order to prevent transparency issues on body def moveUp(): return bpy.ops.object.material_slot_move(direction='UP') while moveUp() != {"CANCELLED"}: pass bpy.ops.object.material_slot_select() else: c.kklog('Material wasn\'t found when separating materials: ' + mat, 'warn') try: bpy.ops.mesh.separate(type='SELECTED') new_object = bpy.context.selected_objects[1] new_object.name = new_object_name return new_object except: c.kklog('Nothing was selected when separating materials from: ' + object.name, 'warn') bpy.ops.object.mode_set(mode = 'OBJECT') return None def delete_materials(self, object: bpy.types.Object, mat_list: bpy.types.Material): '''Deletes the materials in mat_list from object''' for mat in mat_list: if object.data.materials.find(mat.name) > -1: c.switch(object, 'edit') bpy.context.object.active_material_index = object.data.materials.find(mat.name) bpy.ops.object.material_slot_select() bpy.ops.mesh.delete(type='VERT')