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:
BIN
importing/Lut_TimeDay.png
Normal file
BIN
importing/Lut_TimeDay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
importing/__pycache__/importbuttons.cpython-311.pyc
Normal file
BIN
importing/__pycache__/importbuttons.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/modifyarmature.cpython-311.pyc
Normal file
BIN
importing/__pycache__/modifyarmature.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/modifymaterial.cpython-311.pyc
Normal file
BIN
importing/__pycache__/modifymaterial.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/modifymesh.cpython-311.pyc
Normal file
BIN
importing/__pycache__/modifymesh.cpython-311.pyc
Normal file
Binary file not shown.
BIN
importing/__pycache__/postoperations.cpython-311.pyc
Normal file
BIN
importing/__pycache__/postoperations.cpython-311.pyc
Normal file
Binary file not shown.
165
importing/importbuttons.py
Normal file
165
importing/importbuttons.py
Normal 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
2280
importing/modifyarmature.py
Normal file
File diff suppressed because it is too large
Load Diff
117
importing/modifymaterial.md
Normal file
117
importing/modifymaterial.md
Normal 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
1579
importing/modifymaterial.py
Normal file
File diff suppressed because it is too large
Load Diff
138
importing/modifymesh.md
Normal file
138
importing/modifymesh.md
Normal 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
896
importing/modifymesh.py
Normal 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
565
importing/postoperations.py
Normal 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
|
||||
Reference in New Issue
Block a user