# 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