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
This commit is contained in:
2025-12-06 15:26:19 +08:00
commit 5d9e09e9c3
122 changed files with 22683 additions and 0 deletions

BIN
importing/Lut_TimeDay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

165
importing/importbuttons.py Normal file
View File

@@ -0,0 +1,165 @@
'''
This file performs the following operations
· Delete default scene
· Set view transform to Standard
· Create KK log in the scripting tab
· Save the import folder path and character name for later
· Import all pmx files from folder path, then tag them for later
. Invokes the other import operations based on what options were chosen on the panel
'''
import bpy, os, datetime
from ..interface.dictionary_en import t
from .. import common as c
class kkbp_import(bpy.types.Operator):
bl_idname = "kkbp.kkbpimport"
bl_label = "Import .pmx file"
bl_description = t('kkbp_import_tt')
bl_options = {'REGISTER', 'UNDO'}
filepath : bpy.props.StringProperty(maxlen=1024, default='', options={'HIDDEN'})
filter_glob : bpy.props.StringProperty(default='*.pmx', options={'HIDDEN'})
def execute(self, context):
#do this thing because cats does it
if hasattr(bpy.context.scene, 'layers'):
bpy.context.scene.layers[0] = True
#delete the default scene if present
if len(bpy.data.objects) == 3:
for obj in ['Camera', 'Light', 'Cube']:
if bpy.data.objects.get(obj):
bpy.data.objects.remove(bpy.data.objects[obj])
#if the default scene was not present, make sure the default collection is at least there
if not bpy.data.collections.get('Collection'):
new_col = bpy.data.collections.new('Collection')
bpy.context.scene.collection.children.link(new_col)
bpy.context.scene.view_layers[0].active_layer_collection = bpy.context.view_layer.layer_collection.children[new_col.name]
#Set the view transform
bpy.data.scenes[0].display_settings.display_device = 'sRGB'
bpy.context.scene.view_settings.view_transform = 'Standard'
bpy.data.scenes[0].view_settings.look = 'None'
#save filepath for later
bpy.context.scene.kkbp.import_dir = str(self.filepath)[:-9] if self.filepath else bpy.context.scene.kkbp.import_dir
#delete the cached files if the option is enabled
if bpy.context.scene.kkbp.delete_cache and c.get_import_path():
c.kklog('Clearing the cache folder...')
for cache_folder in ['atlas_files', 'baked_files', 'dark_files', 'saturated_files']:
try:
for f in os.listdir(os.path.join(c.get_import_path(), cache_folder)):
try:
os.remove(os.path.join(c.get_import_path(), cache_folder, f))
except:
pass
except:
#that cache folder did not exist
pass
#check if there is at least one "Outfit ##" folder inside of this directory
# if there isn't, then the user incorrectly chose the .pmx file inside of the outfit directory
# correct to the .pmx file inside of the root directory
subdirs = [i[1] for i in os.walk(c.get_import_path())][0]
outfit_subdirs = [i for i in subdirs if 'Outfit ' in i]
if not outfit_subdirs:
bpy.context.scene.kkbp.import_dir = os.path.dirname(os.path.dirname(c.get_import_path()))
c.kklog('User chose wrong pmx file. Defaulting to pmx file located at ' + str(c.get_import_path()), 'warn')
try:
#get the character name and use it for some things later on
bpy.context.scene.kkbp.character_name = c.get_import_path().replace(os.path.dirname(os.path.dirname(c.get_import_path())), '').split('_', maxsplit = 1)[1][:-1]
except:
#the user renamed the export folder, so there was no underscore. Just use the folder name instead (is the name not saved to the json files?)
bpy.context.scene.kkbp.character_name = c.get_import_path().replace(os.path.dirname(os.path.dirname(c.get_import_path())), '')[1:-1]
#remove any dots from the character name or blender will get confused when creating an atlas
bpy.context.scene.kkbp.character_name = bpy.context.scene.kkbp.character_name.replace('.', '')
#but if the name is longer than 64 characters, blender will cut off the name, leading to some issues later on.
#The longest material I've encountered is "KK acs_M_nose_tama_00 1290 " and the longest suffix will always be " light.png" at 37 total characters
#so I'll arbitrarily cut off the name at 24 characters to be safe (needs to be an even number). The .encode() is used to handle multibyte characters like japanese / korean names
if len(bpy.context.scene.kkbp.character_name.encode()) >= 24:
bpy.context.scene.kkbp.character_name = bpy.context.scene.kkbp.character_name.encode()[:24].decode()
c.json_file_manager.init()
#force pmx armature selection if exportCurrentPose in the Exporter Config json is true
force_current_pose = c.json_file_manager.get_json_file('KK_KKBPExporterConfig.json')['exportCurrentPose']
if force_current_pose:
bpy.context.scene.kkbp.armature_dropdown = 'C'
#force no dark colors if Cycles classic is chosen as the shader (this mode does not use dark colors at all)
if bpy.context.scene.kkbp.shader_dropdown == 'D':
bpy.context.scene.kkbp.colors_dropdown = False
functions = [
lambda:bpy.ops.kkbp.modifymesh('INVOKE_DEFAULT'),
lambda:bpy.ops.kkbp.modifyarmature('INVOKE_DEFAULT'),
lambda:bpy.ops.kkbp.modifymaterial('INVOKE_DEFAULT'),
lambda:bpy.ops.kkbp.postoperations('INVOKE_DEFAULT'),
]
#run functions
c.toggle_console()
self.import_pmx_models()
for index, function in enumerate(functions):
print('Import function {} running'.format(index))
function()
c.toggle_console()
bpy.context.scene.kkbp.plugin_state = 'imported'
c.kklog('KKBP import finished in {} minutes'.format(round(((datetime.datetime.now().minute * 60 + datetime.datetime.now().second + datetime.datetime.now().microsecond / 1e6) - bpy.context.scene.kkbp.total_timer) / 60, 2)))
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def import_pmx_models(self):
c.kklog('Importing pmx files with mmdtools...')
for subdir, dirs, files in os.walk(c.get_import_path()):
for file in [f for f in files if f == 'model.pmx']:
pmx_path = os.path.join(subdir, file)
outfit = 'Outfit' in subdir
#import the pmx file with mmd_tools
if bpy.app.version[0] == 3:
bpy.ops.mmd_tools.import_model('EXEC_DEFAULT',
files=[{'name': pmx_path}],
directory=pmx_path,
scale=1,
clean_model = False,
types={'MESH', 'ARMATURE', 'MORPHS'} if not outfit else {'MESH'},
log_level='WARNING')
else:
bpy.ops.mmd_tools.import_model('EXEC_DEFAULT',
filepath=pmx_path,
scale=1,
clean_model = False,
types={'MESH', 'ARMATURE', 'MORPHS'} if not outfit else {'MESH', 'ARMATURE'})
#tag the newly import object after pmx import. The active object is the empty, so apply it to the armature and the mesh
bpy.context.view_layer.objects.active['name'] = c.get_name()
bpy.context.view_layer.objects.active.children[0]['name'] = c.get_name()
bpy.context.view_layer.objects.active.children[0].children[0]['name'] = c.get_name()
#keep track of the outfit ID if this is an outfit
if outfit:
bpy.context.view_layer.objects.active.children[0].children[0]['id'] = str(subdir[-2:])
bpy.context.view_layer.objects.active.children[0].children[0]['outfit'] = True
bpy.context.view_layer.objects.active.children[0].children[0].name = 'Outfit ' + str(subdir[-2:]) + ' ' + c.get_name()
else:
bpy.context.view_layer.objects.active.children[0].children[0].name = 'Body ' + c.get_name()
bpy.context.view_layer.objects.active.children[0].children[0]['body'] = True
bpy.context.view_layer.objects.active.children[0].name = 'Armature ' + c.get_name()
bpy.context.view_layer.objects.active.children[0]['armature'] = True
#get rid of the text files the mmd tools addon generates
if bpy.data.texts.get('Model'):
bpy.data.texts.remove(bpy.data.texts['Model'])
bpy.data.texts.remove(bpy.data.texts['Model_e'])
#rename the collection to the character name
bpy.data.collections['Collection'].name = c.get_name()
c.initialize_timer()
c.print_timer('Import PMX')

2280
importing/modifyarmature.py Normal file

File diff suppressed because it is too large Load Diff

117
importing/modifymaterial.md Normal file
View File

@@ -0,0 +1,117 @@
### Texture Postfix Legend:
_MT_CT -> _MainTex_ColorTexture
_MT_DT -> _MainTex_DarkenedColorTexture (aka DarkTex)
_MT -> _MainTex (aka Plain MainTex)
_ST_CT -> _Saturated_MainTex_ColorTexture
_ST_DT -> _Saturated_MainTex_DarkenedColorTexture (aka Saturated DarkTex)
_ST -> _Saturated_MainTex (aka Plain Saturated MainTex)
_AM -> _AlphaMask
_CM -> _ColorMask
_DM -> _DetailMask
_LM -> _LineMask
_NM -> _NormalMask
_NMP -> _NormalMap
_NMPD -> _NormalMapDetail
_ot1 -> _overtex1
_ot2 -> _overtex2
_ot3 -> _overtex3
_lqdm -> _liquidmask
_HGLS -> _HairGloss
_T2 -> _Texture2
_T3 -> _Texture3
_T4 -> _Texture4
_T5 -> _Texture5
_T6 -> _Texture6
_T7 -> _Texture7
_PM1 -> _PatternMask1
_PM1 -> _PatternMask2
_PM1 -> _PatternMask3
### stripped down clothes source for dark colors
float3 diffuse = mainTex * color;
float3 shadingAdjustment = ShadeAdjustItem(diffuse);
float3 diffuseShaded = shadingAdjustment * 0.899999976 - 0.5;
diffuseShaded = -diffuseShaded * 2 + 1;
bool3 compTest = 0.555555582 < shadingAdjustment;
shadingAdjustment *= 1.79999995;
diffuseShaded = -diffuseShaded * 0.7225 + 1;
{
float3 hlslcc_movcTemp = shadingAdjustment;
hlslcc_movcTemp.x = (compTest.x) ? diffuseShaded.x : shadingAdjustment.x;
hlslcc_movcTemp.y = (compTest.y) ? diffuseShaded.y : shadingAdjustment.y;
hlslcc_movcTemp.z = (compTest.z) ? diffuseShaded.z : shadingAdjustment.z;
shadingAdjustment = saturate(hlslcc_movcTemp);
}
float3 diffuseShadow = diffuse * shadingAdjustment;
float3 lightCol = float3(1.0656, 1.0656, 1.0656); // constant calculated from custom ambient .666 and light color .666
float3 ambientCol = max(lightCol, .15);
diffuseShadow = diffuseShadow * ambientCol;
return float4(diffuseShadow, 1);
### stripped down skin source for dark colors
//Diffuse and color maps KK uses for shading I assume
float3 diffuse = GetDiffuse(i);
float3 specularAdjustment; //Adjustments for specular from detailmap
float3 shadingAdjustment; //Adjustments for shading
MapValuesMain(diffuse, specularAdjustment, shadingAdjustment);
//Shading
float3 diffuseShaded = shadingAdjustment * 0.899999976 - 0.5;
diffuseShaded = -diffuseShaded * 2 + 1;
bool3 compTest = 0.555555582 < shadingAdjustment;
shadingAdjustment *= 1.79999995;
diffuseShaded = -diffuseShaded * 0.7225 + 1;
{
float3 hlslcc_movcTemp = shadingAdjustment;
hlslcc_movcTemp.x = (compTest.x) ? diffuseShaded.x : shadingAdjustment.x;
hlslcc_movcTemp.y = (compTest.y) ? diffuseShaded.y : shadingAdjustment.y;
hlslcc_movcTemp.z = (compTest.z) ? diffuseShaded.z : shadingAdjustment.z;
shadingAdjustment = saturate(hlslcc_movcTemp);
}
float3 finalDiffuse = diffuse * shadingAdjustment;
float3 bodyShine = float3(1.0656, 1.0656, 1.0656);
finalDiffuse *= bodyShine;
return float4(finalDiffuse, 1);
### stripped down hair source for dark colors
float3 diffuse = GetDiffuse(i.uv0) * mainTex.rgb;
float3 finalAmbientShadow = 0.7225;
finalAmbientShadow = saturate(finalAmbientShadow);
float3 invertFinalAmbientShadow = 0.7225;
finalAmbientShadow = finalAmbientShadow * _ShadowColor.xyz;
finalAmbientShadow += finalAmbientShadow;
float3 shadowCol = _ShadowColor - 0.5;
shadowCol = -shadowCol * 2 + 1;
invertFinalAmbientShadow = -shadowCol * invertFinalAmbientShadow + 1;
bool3 shadeCheck = 0.5 < _ShadowColor.xyz;
{
float3 hlslcc_movcTemp = finalAmbientShadow;
hlslcc_movcTemp.x = (shadeCheck.x) ? invertFinalAmbientShadow.x : finalAmbientShadow.x;
hlslcc_movcTemp.y = (shadeCheck.y) ? invertFinalAmbientShadow.y : finalAmbientShadow.y;
hlslcc_movcTemp.z = (shadeCheck.z) ? invertFinalAmbientShadow.z : finalAmbientShadow.z;
finalAmbientShadow = hlslcc_movcTemp;
}
finalAmbientShadow = saturate(finalAmbientShadow);
diffuse *= finalAmbientShadow;
float3 finalDiffuse = saturate(diffuse);
float3 shading = 1 - finalAmbientShadow;
shading = 1 * shading + finalAmbientShadow;
finalDiffuse *= shading;
shading = 1.0656;
finalDiffuse *= shading;
return float4(finalDiffuse, alpha);

1579
importing/modifymaterial.py Normal file

File diff suppressed because it is too large Load Diff

138
importing/modifymesh.md Normal file
View File

@@ -0,0 +1,138 @@
### ENUM order that corresponds to ChaReference_RefObjKey value in KK_ReferenceInfoData.json
0. HeadParent,
1. HairParent,
1. a_n_hair_pony,
1. a_n_hair_twin_L,
1. a_n_hair_twin_R,
1. a_n_hair_pin,
1. a_n_hair_pin_R,
1. a_n_headtop,
1. a_n_headflont,
1. a_n_head,
1. a_n_headside,
1. a_n_megane,
1. a_n_earrings_L,
1. a_n_earrings_R,
1. a_n_nose,
1. a_n_mouth,
1. a_n_neck,
1. a_n_bust_f,
1. a_n_bust,
1. a_n_nip_L,
1. a_n_nip_R,
1. a_n_back,
1. a_n_back_L,
1. a_n_back_R,
1. a_n_waist,
1. a_n_waist_f,
1. a_n_waist_b,
1. a_n_waist_L,
1. a_n_waist_R,
1. a_n_leg_L,
1. a_n_leg_R,
1. a_n_knee_L,
1. a_n_knee_R,
1. a_n_ankle_L,
1. a_n_ankle_R,
1. a_n_heel_L,
1. a_n_heel_R,
1. a_n_shoulder_L,
1. a_n_shoulder_R,
1. a_n_elbo_L,
1. a_n_elbo_R,
1. a_n_arm_L,
1. a_n_arm_R,
1. a_n_wrist_L,
1. a_n_wrist_R,
1. a_n_hand_L,
1. a_n_hand_R,
1. a_n_ind_L,
1. a_n_ind_R,
1. a_n_mid_L,
1. a_n_mid_R,
1. a_n_ring_L,
1. a_n_ring_R,
1. a_n_dan,
1. a_n_kokan,
1. a_n_ana,
1. k_f_handL_00,
1. k_f_handR_00,
1. k_f_shoulderL_00,
1. k_f_shoulderR_00,
1. ObjEyeline,
1. ObjEyelineLow,
1. ObjEyebrow,
1. ObjNoseline,
1. ObjEyeL,
1. ObjEyeR,
1. ObjEyeWL,
1. ObjEyeWR,
1. ObjFace,
1. ObjDoubleTooth,
1. ObjBody,
1. ObjNip,
1. N_FaceSpecial,
1. CORRECT_ARM_L,
1. CORRECT_ARM_R,
1. CORRECT_HAND_L,
1. CORRECT_HAND_R,
1. CORRECT_TONGUE_TOP,
1. CORRECT_MOUTH_TARGET,
1. CORRECT_MOUTH_TARGET02,
1. CORRECT_HEAD_DBCOL,
1. S_ANA,
1. S_TongueF,
1. S_TongueB,
1. S_Son,
1. S_SimpleTop,
1. S_SimpleBody,
1. S_SimpleTongue,
1. S_MNPA,
1. S_MNPB,
1. S_MOZ_ALL,
1. S_GOMU,
1. S_CTOP_T_DEF,
1. S_CTOP_T_NUGE,
1. S_CTOP_B_DEF,
1. S_CTOP_B_NUGE,
1. S_CBOT_T_DEF,
1. S_CBOT_T_NUGE,
1. S_CBOT_B_DEF,
1. S_CBOT_B_NUGE,
1. S_UWT_T_DEF,
1. S_UWT_T_NUGE,
1. S_UWT_B_DEF,
1. S_UWT_B_NUGE,
1. S_UWB_T_DEF,
1. S_UWB_T_NUGE,
1. S_UWB_B_DEF,
1. S_UWB_B_NUGE,
1. S_UWB_B_NUGE2,
1. S_PANST_DEF,
1. S_PANST_NUGE,
1. S_TPARTS_00_DEF,
1. S_TPARTS_00_NUGE,
1. S_TPARTS_01_DEF,
1. S_TPARTS_01_NUGE,
1. S_TPARTS_02_DEF,
1. S_TPARTS_02_NUGE,
1. ObjBraDef,
1. ObjBraNuge,
1. ObjInnerDef,
1. ObjInnerNuge,
1. S_TEARS_01,
1. S_TEARS_02,
1. S_TEARS_03,
1. N_EyeBase,
1. N_Hitomi,
1. N_Gag00,
1. N_Gag01,
1. N_Gag02,
1. DB_SKIRT_TOP,
1. DB_SKIRT_TOPA,
1. DB_SKIRT_TOPB,
1. DB_SKIRT_BOT,
1. F_ADJUSTWIDTHSCALE,
1. A_ROOTBONE,
1. BUSTUP_TARGET,
1. NECK_LOOK_TARGET

896
importing/modifymesh.py Normal file
View File

@@ -0,0 +1,896 @@
'''
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')

565
importing/postoperations.py Normal file
View File

@@ -0,0 +1,565 @@
# 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