- 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
339 lines
14 KiB
Python
339 lines
14 KiB
Python
#simplfies bone count using the merge weights function in CATS
|
|
|
|
import bpy, traceback, time
|
|
from .. import common as c
|
|
from ..interface.dictionary_en import t
|
|
|
|
def main(prep_type, simp_type):
|
|
try:
|
|
#always try to use the atlased model first
|
|
body = bpy.data.objects['Body ' + c.get_name() + '.001']
|
|
bpy.context.view_layer.objects.active=body
|
|
body_name = body.name
|
|
armature_name = 'Armature.001'
|
|
if not bpy.data.objects[armature_name].data.bones.get('Pelvis'):
|
|
#the atlased body has already been modified. Skip.
|
|
c.kklog('Model with atlas has already been prepped. Skipping export prep functions...', type='warn')
|
|
return False
|
|
except:
|
|
#fallback to the non-atlased model if the atlased model collection is not visible
|
|
body = bpy.data.objects['Body ' + c.get_name()]
|
|
bpy.context.view_layer.objects.active=body
|
|
body_name = body.name
|
|
armature_name = 'Armature'
|
|
|
|
armature = bpy.data.objects[armature_name]
|
|
|
|
c.kklog('\nPrepping for export...')
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
#Assume hidden items are unused and move them to their own collection
|
|
c.kklog('Moving unused objects to their own collection...')
|
|
no_move_objects = ['Hitboxes ' + c.get_name(), body_name, armature_name]
|
|
for object in bpy.context.scene.objects:
|
|
try:
|
|
move_this_one = object.name not in no_move_objects and 'Widget' not in object.name and object.hide_get()
|
|
if move_this_one:
|
|
object.hide_set(False)
|
|
object.select_set(True)
|
|
bpy.context.view_layer.objects.active=object
|
|
except:
|
|
c.kklog("During export prep, couldn't move object '{}' for some reason...".format(object), type='error')
|
|
if bpy.context.selected_objects:
|
|
bpy.ops.object.move_to_collection(collection_index=0, is_new=True, new_collection_name='Unused clothing items')
|
|
#hide the new collection
|
|
try:
|
|
bpy.context.scene.view_layers[0].active_layer_collection = bpy.context.view_layer.layer_collection.children['Unused clothing items']
|
|
bpy.context.scene.view_layers[0].active_layer_collection.exclude = True
|
|
except:
|
|
try:
|
|
#maybe the collection is in the default Collection collection
|
|
bpy.context.scene.view_layers[0].active_layer_collection = bpy.context.view_layer.layer_collection.children['Collection'].children['Unused clothing items']
|
|
bpy.context.scene.view_layers[0].active_layer_collection.exclude = True
|
|
except:
|
|
#maybe the collection is already hidden, or doesn't exist
|
|
pass
|
|
|
|
c.kklog('Removing object outline modifier...')
|
|
for ob in bpy.data.objects:
|
|
if ob.modifiers.get('Outline Modifier'):
|
|
ob.modifiers['Outline Modifier'].show_render = False
|
|
ob.modifiers['Outline Modifier'].show_viewport = False
|
|
#remove the outline materials because they won't be baked
|
|
if ob in [obj for obj in bpy.context.view_layer.objects if obj.type == 'MESH']:
|
|
ob.select_set(True)
|
|
bpy.context.view_layer.objects.active=ob
|
|
bpy.ops.object.material_slot_remove_unused()
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
body = bpy.data.objects[body_name]
|
|
bpy.context.view_layer.objects.active=body
|
|
body.select_set(True)
|
|
|
|
#Select the armature and make it active
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
bpy.data.objects[armature_name].hide_set(False)
|
|
bpy.data.objects[armature_name].select_set(True)
|
|
bpy.context.view_layer.objects.active=bpy.data.objects[armature_name]
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
|
|
# If exporting for Unreal...
|
|
if prep_type == 'E':
|
|
armature = bpy.data.objects[armature_name]
|
|
bpy.context.view_layer.objects.active = armature
|
|
bpy.ops.armature.collection_show_all()
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
#Clear IK, it won't work in unreal
|
|
for bone in armature.pose.bones:
|
|
for constraint in bone.constraints:
|
|
bone.constraints.remove(constraint)
|
|
|
|
#Rename some bones to make it match Mannequin skeleton
|
|
#Not necessary, but allows Unreal automatically recognize and match bone names when retargeting
|
|
ue_rename_dict = {
|
|
'Hips': 'pelvis',
|
|
'Spine': 'spine_01',
|
|
'Chest': 'spine_02',
|
|
'Upper Chest': 'spine_03',
|
|
'Neck': 'neck',
|
|
'Head': 'head',
|
|
'Left shoulder': 'clavicle_l',
|
|
'Right shoulder': 'clavicle_r',
|
|
'Left arm': 'upperarm_l',
|
|
'Right arm': 'upperarm_r',
|
|
'Left elbow': 'lowerarm_l',
|
|
'Right elbow': 'lowerarm_r',
|
|
'Left wrist': 'hand_l',
|
|
'Right wrist': 'hand_r',
|
|
|
|
'Left leg': 'thigh_l',
|
|
'Right leg': 'thigh_r',
|
|
'Left knee': 'calf_l',
|
|
'Right knee': 'calf_r',
|
|
'cf_j_leg03_L': 'foot_l',
|
|
'cf_j_leg03_R': 'foot_r',
|
|
'Left toe': 'ball_l',
|
|
'Right toe': 'ball_r',
|
|
}
|
|
for bone in ue_rename_dict:
|
|
if armature.data.bones.get(bone):
|
|
armature.data.bones[bone].name = ue_rename_dict[bone]
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
#Make all the bones on the legs face the same direction, otherwise IK won't work in Unreal
|
|
armature.data.edit_bones["calf_l"].tail.z = armature.data.edit_bones["calf_l"].head.z + 0.1
|
|
armature.data.edit_bones["calf_l"].head.y += 0.01
|
|
armature.data.edit_bones["calf_r"].tail.z = armature.data.edit_bones["calf_r"].head.z + 0.1
|
|
armature.data.edit_bones["calf_r"].head.y += 0.01
|
|
|
|
armature.data.edit_bones["ball_l"].tail.z = armature.data.edit_bones["ball_l"].head.z
|
|
armature.data.edit_bones["ball_l"].tail.y = armature.data.edit_bones["ball_l"].head.y - 0.05
|
|
armature.data.edit_bones["ball_r"].tail.z = armature.data.edit_bones["ball_r"].head.z
|
|
armature.data.edit_bones["ball_r"].tail.y = armature.data.edit_bones["ball_r"].head.y - 0.05
|
|
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
|
|
#If simplifying the bones...
|
|
if simp_type in ['A', 'B']:
|
|
#show all bones on the armature
|
|
bpy.ops.armature.collection_show_all()
|
|
bpy.ops.pose.select_all(action='DESELECT')
|
|
|
|
#Move pupil bones to layer 1
|
|
armature = bpy.data.objects[armature_name]
|
|
if armature.data.bones.get('Left Eye'):
|
|
armature.data.bones['Left Eye'].collections.clear()
|
|
armature.data.collections['0'].assign(armature.data.bones.get('Left Eye'))
|
|
armature.data.bones['Right Eye'].collections.clear()
|
|
armature.data.collections['0'].assign(armature.data.bones.get('Right Eye'))
|
|
|
|
#Select bones on layer 11
|
|
for bone in armature.data.bones:
|
|
if bone.collections.get('10'):
|
|
bone.select = True
|
|
|
|
#if very simple selected, also get 3-5,12,17-19
|
|
if simp_type in ['A']:
|
|
for bone in armature.data.bones:
|
|
select_bool = (bone.collections.get('2') or
|
|
bone.collections.get('3') or
|
|
bone.collections.get('4') or
|
|
bone.collections.get('11') or
|
|
bone.collections.get('12') or
|
|
bone.collections.get('16') or
|
|
bone.collections.get('17') or
|
|
bone.collections.get('18')
|
|
)
|
|
if select_bool:
|
|
bone.select = True
|
|
|
|
c.kklog('Using the merge weights function in CATS to simplify bones...')
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.kkbp.cats_merge_weights()
|
|
|
|
#If exporting for VRM or VRC...
|
|
if prep_type in ['A', 'D']:
|
|
c.kklog('Editing armature for VRM...')
|
|
bpy.context.view_layer.objects.active=armature
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
#Rearrange bones to match CATS output
|
|
if armature.data.edit_bones.get('Pelvis'):
|
|
armature.data.edit_bones['Pelvis'].parent = None
|
|
armature.data.edit_bones['Spine'].parent = armature.data.edit_bones['Pelvis']
|
|
armature.data.edit_bones['Hips'].name = 'dont need lol'
|
|
armature.data.edit_bones['Pelvis'].name = 'Hips'
|
|
armature.data.edit_bones['Left leg'].parent = armature.data.edit_bones['Hips']
|
|
armature.data.edit_bones['Right leg'].parent = armature.data.edit_bones['Hips']
|
|
armature.data.edit_bones['Left ankle'].parent = armature.data.edit_bones['Left knee']
|
|
armature.data.edit_bones['Right ankle'].parent = armature.data.edit_bones['Right knee']
|
|
armature.data.edit_bones['Left shoulder'].parent = armature.data.edit_bones['Upper Chest']
|
|
armature.data.edit_bones['Right shoulder'].parent = armature.data.edit_bones['Upper Chest']
|
|
armature.data.edit_bones.remove(armature.data.edit_bones['dont need lol'])
|
|
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
bpy.ops.pose.select_all(action='DESELECT')
|
|
|
|
#Merge specific bones for unity rig autodetect
|
|
armature = bpy.data.objects[armature_name]
|
|
merge_these = ['cf_j_waist02', 'cf_s_waist01', 'cf_s_hand_L', 'cf_s_hand_R']
|
|
#Delete the upper chest for VR chat models, since it apparently causes errors with eye tracking
|
|
if prep_type == 'D':
|
|
merge_these.append('Upper Chest')
|
|
for bone in armature.data.bones:
|
|
if bone.name in merge_these:
|
|
bone.select = True
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
bpy.ops.kkbp.cats_merge_weights()
|
|
|
|
#If exporting for MMD...
|
|
if prep_type == 'C':
|
|
#Create the empty
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.empty_add(type='PLAIN_AXES', align='WORLD', location=(0, 0, 0))
|
|
empty = bpy.data.objects['Empty']
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
armature.parent = empty
|
|
bpy.context.view_layer.objects.active = armature
|
|
|
|
#rename bones to stock
|
|
if armature.data.bones.get('Center'):
|
|
bpy.ops.kkbp.switcharmature('INVOKE_DEFAULT')
|
|
|
|
#then rename bones to japanese
|
|
pmx_rename_dict = {
|
|
'全ての親':'cf_n_height',
|
|
'センター':'cf_j_hips',
|
|
'上半身':'cf_j_spine01',
|
|
'上半身2':'cf_j_spine02',
|
|
'上半身3':'cf_j_spine03',
|
|
'首':'cf_j_neck',
|
|
'頭':'cf_j_head',
|
|
'両目':'Eyesx',
|
|
'左目':'cf_J_hitomi_tx_L',
|
|
'右目':'cf_J_hitomi_tx_R',
|
|
'左腕':'cf_j_arm00_L',
|
|
'右腕':'cf_j_arm00_R',
|
|
'左ひじ':'cf_j_forearm01_L',
|
|
'右ひじ':'cf_j_forearm01_R',
|
|
'左肩':'cf_j_shoulder_L',
|
|
'右肩':'cf_j_shoulder_R',
|
|
'左手首':'cf_j_hand_L',
|
|
'右手首':'cf_j_hand_R',
|
|
'左親指0':'cf_j_thumb01_L',
|
|
'左親指1':'cf_j_thumb02_L',
|
|
'左親指2':'cf_j_thumb03_L',
|
|
'左薬指1':'cf_j_ring01_L',
|
|
'左薬指2':'cf_j_ring02_L',
|
|
'左薬指3':'cf_j_ring03_L',
|
|
'左中指1':'cf_j_middle01_L',
|
|
'左中指2':'cf_j_middle02_L',
|
|
'左中指3':'cf_j_middle03_L',
|
|
'左小指1':'cf_j_little01_L',
|
|
'左小指2':'cf_j_little02_L',
|
|
'左小指3':'cf_j_little03_L',
|
|
'左人指1':'cf_j_index01_L',
|
|
'左人指2':'cf_j_index02_L',
|
|
'左人指3':'cf_j_index03_L',
|
|
'右親指0':'cf_j_thumb01_R',
|
|
'右親指1':'cf_j_thumb02_R',
|
|
'右親指2':'cf_j_thumb03_R',
|
|
'右薬指1':'cf_j_ring01_R',
|
|
'右薬指2':'cf_j_ring02_R',
|
|
'右薬指3':'cf_j_ring03_R',
|
|
'右中指1':'cf_j_middle01_R',
|
|
'右中指2':'cf_j_middle02_R',
|
|
'右中指3':'cf_j_middle03_R',
|
|
'右小指1':'cf_j_little01_R',
|
|
'右小指2':'cf_j_little02_R',
|
|
'右小指3':'cf_j_little03_R',
|
|
'右人指1':'cf_j_index01_R',
|
|
'右人指2':'cf_j_index02_R',
|
|
'右人指3':'cf_j_index03_R',
|
|
'下半身':'cf_j_waist01',
|
|
'左足':'cf_j_thigh00_L',
|
|
'右足':'cf_j_thigh00_R',
|
|
'左ひざ':'cf_j_leg01_L',
|
|
'右ひざ':'cf_j_leg01_R',
|
|
'左足首':'cf_j_leg03_L',
|
|
'右足首':'cf_j_leg03_R',
|
|
}
|
|
|
|
for bone in pmx_rename_dict:
|
|
armature.data.bones[pmx_rename_dict[bone]].name = bone
|
|
|
|
#Rearrange bones to match a random pmx model I found
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
armature.data.edit_bones['左肩'].parent = armature.data.edit_bones['上半身3']
|
|
armature.data.edit_bones['右肩'].parent = armature.data.edit_bones['上半身3']
|
|
armature.data.edit_bones['左足'].parent = armature.data.edit_bones['下半身']
|
|
armature.data.edit_bones['右足'].parent = armature.data.edit_bones['下半身']
|
|
|
|
#refresh the vertex groups? Bones will act as if they're detached if this isn't done
|
|
body.vertex_groups.active=body.vertex_groups['BodyTop']
|
|
|
|
#combine all objects into one
|
|
|
|
#create leg IKs?
|
|
|
|
c.kklog('Using CATS to simplify more bones for MMD...')
|
|
|
|
#use mmd_tools to convert
|
|
bpy.ops.mmd_tools.convert_to_mmd_model()
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
#only disable the prep button if the non-atlas model has been modified.
|
|
#This is because the model with atlas can be regenerated with the bake materials button
|
|
return armature_name == 'Armature'
|
|
|
|
class export_prep(bpy.types.Operator):
|
|
bl_idname = "kkbp.exportprep"
|
|
bl_label = "Prep for target application"
|
|
bl_description = t('export_prep_tt')
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
scene = context.scene.kkbp
|
|
prep_type = scene.prep_dropdown
|
|
simp_type = scene.simp_dropdown
|
|
last_step = time.time()
|
|
try:
|
|
c.toggle_console()
|
|
if main(prep_type, simp_type):
|
|
scene.plugin_state = 'prepped'
|
|
c.kklog('Finished in ' + str(time.time() - last_step)[0:4] + 's')
|
|
c.toggle_console()
|
|
return {'FINISHED'}
|
|
except:
|
|
c.kklog('Unknown python error occurred', type = 'error')
|
|
c.kklog(traceback.format_exc())
|
|
self.report({'ERROR'}, traceback.format_exc())
|
|
return {"CANCELLED"}
|
|
|