feat: Add KoikatsuBlenderPipeline addon with import/export functionality

- Add core addon files (__init__.py, KKPanel.py, preferences.py, common.py)
- Add import pipeline with armature, mesh, and material modification modules
- Add export pipeline with material baking and FBX preparation utilities
- Add material combiner tool for texture atlas generation and optimization
- Add extras utilities for animation, rigging, and asset management
- Add bone orientation data from better_fbx for accurate skeletal structure
- Add comprehensive documentation and wiki with multi-language support (EN, JP, ZH)
- Add animation library retargeting lists for ARP and Rokoko motion capture
- Add Rigify integration scripts for advanced rigging workflows
- Add shader file (KK Shader V8.0.blend) for material rendering
- Add manifest and license files for addon distribution
- Add changelog documenting version history and improvements
- Initialize complete Blender addon project for Koikatsu character import/export
This commit is contained in:
2025-12-06 15:26:19 +08:00
commit 5d9e09e9c3
122 changed files with 22683 additions and 0 deletions

315
extras/importanimation.py Normal file
View File

@@ -0,0 +1,315 @@
import bpy, os, time, mathutils, json, pathlib
from .. import common as c
from ..interface.dictionary_en import t
def main(file):
kkbp_character = False
#delete the armature before starting to reduce console spam
if c.get_body() and c.get_rig():
kkbp_character = True
c.toggle_console() #open console for some kind of progression
start = time.time()
rigify_armature = bpy.data.objects['RIG-Armature']
bpy.context.view_layer.objects.active=rigify_armature
bpy.ops.object.mode_set(mode = 'OBJECT')
bpy.ops.rsl.clear_custom_bones()
if kkbp_character:
#disable IKs
rigify_armature.pose.bones["Right arm_parent"]["IK_FK"] = 1
rigify_armature.pose.bones["Left arm_parent"]["IK_FK"] = 1
rigify_armature.pose.bones["Right leg_parent"]["IK_FK"] = 1
rigify_armature.pose.bones["Left leg_parent"]["IK_FK"] = 1
#disable head follow
rigify_armature.pose.bones['torso']['neck_follow'] = 1.0
rigify_armature.pose.bones['torso']['head_follow'] = 1.0
bpy.ops.import_scene.fbx(filepath=str(file), global_scale=96)
imported_armature = bpy.context.object
#save the source action name so I can delete it later
current_action_name = imported_armature.animation_data.action.name
#setup rokoko remapping
my_areas = bpy.context.workspace.screens[0].areas
for area in my_areas:
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_object_viewport_armature = True
bpy.context.scene.rsl_retargeting_armature_source = imported_armature
bpy.context.scene.rsl_retargeting_armature_target = rigify_armature
bpy.ops.rsl.build_bone_list()
#setup all remapping stuff if this is the first imported animation
script_dir=pathlib.Path(__file__).parent
if bpy.context.scene.kkbp.animation_import_type:
remap_file = os.path.join(script_dir, "animationlibrary", "Rokoko retargeting list (mixamo to KKBP Rigify).json")
else:
remap_file = os.path.join(script_dir, "animationlibrary", "Rokoko retargeting list (koikatsu fbx to KKBP Rigify).json")
if kkbp_character:
script_dir=pathlib.Path(__file__).parent
json_file = open(remap_file)
data = json.load(json_file)
for row in bpy.context.scene.rsl_retargeting_bone_list:
row.bone_name_target = "" #clear all bones
for bone in data['bones']:
for row in bpy.context.scene.rsl_retargeting_bone_list:
if row.bone_name_source.lower() == data['bones'][bone][0].lower():
row.bone_name_target = data['bones'][bone][1]
bpy.ops.rsl.retarget_animation()
# then remove the fcurves of the retargeted bones that were not present in the retargeting list.
# A lot of popping and fluctuation is present if these aren't removed
retargeting_list = [
'torso',
'Spine_fk',
'Chest_fk',
'Upper Chest_fk',
'Left arm_fk',
'Left elbow_fk',
'Left wrist_fk',
'IndexFinger1_L',
'IndexFinger2_L',
'IndexFinger3_L',
'LittleFinger1_L',
'LittleFinger2_L',
'LittleFinger3_L',
'MiddleFinger1_L',
'MiddleFinger2_L',
'MiddleFinger3_L',
'RingFinger1_L',
'RingFinger2_L',
'RingFinger3_L',
'Thumb0_L',
'Thumb1_L',
'Thumb2_L',
'Right arm_fk',
'Right elbow_fk',
'Right wrist_fk',
'IndexFinger1_R',
'IndexFinger2_R',
'IndexFinger3_R',
'LittleFinger1_R',
'LittleFinger2_R',
'LittleFinger3_R',
'MiddleFinger1_R',
'MiddleFinger2_R',
'MiddleFinger3_R',
'RingFinger1_R',
'RingFinger2_R',
'RingFinger3_R',
'Thumb0_R',
'Thumb1_R',
'Thumb2_R',
'neck',
'head',
'Hips_fk',
'Left leg_fk',
'Left knee_fk',
'Left ankle_fk',
'Left toe_fk',
'Right leg_fk',
'Right knee_fk',
'Right ankle_fk',
'Right toe_fk',
'Left shoulder',
'cf_j_waist02',
'Right shoulder',
'cf_j_kokan',
#'cf_j_ana',
'Breasts handle',
'Left Breast handle',
'cf_j_bust02_l',
'cf_j_bust03_l',
'cf_j_bnip02root_l',
'cf_j_bnip02_l',
'Right Breast handle',
'cf_j_bust02_r',
'cf_j_bust03_r',
'cf_j_bnip02root_r',
'cf_j_bnip02_r',
#'Left Buttock handle',
#'Right Buttock handle',
]
if kkbp_character:
for index, action in enumerate(rigify_armature.animation_data.action.fcurves):
bone_name = action.data_path[action.data_path.find('[')+2:action.data_path.find('].')-1]
print('{} {} / {}'.format(action.data_path, index, len(rigify_armature.animation_data.action.fcurves)))
if bone_name not in retargeting_list:
rigify_armature.animation_data.action.fcurves.remove(action) #remove keyframes
if rigify_armature.pose.bones.get(bone_name):
rigify_armature.pose.bones[bone_name].matrix_basis = mathutils.Matrix() #reset rotation matrix
if bpy.context.scene.kkbp.animation_library_scale:
#also scale the arms on the y axis by 5% because it makes the animation more accurate to the in-game one for some poses
already_got_both_arms = False
if kkbp_character:
for y_scale_curve in [curve for curve in rigify_armature.animation_data.action.fcurves if (curve.array_index == 1) and ('scale' in curve.data_path)]:
bone_name = y_scale_curve.data_path[y_scale_curve.data_path.find('[')+2:y_scale_curve.data_path.find('].')-1]
if bone_name in ['Left arm_fk', 'Right arm_fk']:
keyframe_limits = [int(y_scale_curve.keyframe_points[0].co.x), int(y_scale_curve.keyframe_points[-1].co.x)]
for frame_number in range(keyframe_limits[0], keyframe_limits[1]+1):
original_scale = y_scale_curve.evaluate(frame_number)
y_scale_curve.keyframe_points.insert(frame_number, original_scale * 1.05)
if already_got_both_arms:
break #skip the rest
already_got_both_arms = True
#select all rigify armature bones and create pose asset
bpy.ops.object.select_all(action='DESELECT')
rigify_armature.select_set(True)
bpy.context.view_layer.objects.active=rigify_armature
bpy.ops.object.mode_set(mode = 'POSE')
bpy.ops.pose.select_all(action='SELECT')
bpy.data.workspaces["Layout"].asset_library_reference = 'LOCAL'
#translate name
name = file.replace('-p_cf_body_bone-0.fbx', '')
name = name.replace('-p_cf_body_bone-1.fbx', '')
translation_dict_normal = {
'isu_':'Seated_',
'suwari':'Sitting',
'syagami':'Squat',
'sadou':'Tea',
'undou':'Excercise',
'suiei':'Swimming',
'tachi':'Standing',
'manken':'Reading',
'soine':'Laying2',
'kasa':'Umbrella',
'konbou':'Club',
'aruki':'Walking',
'haruki':'Running',
'ne_0':'Sleep',
'nugi':'Undressing',
}
translation_dict_h = {
'_A_Idle':'Crotch grope idle',
'_A_Loop':'Crotch grope',
'_A_Touch':'Crotch grope start',
'_K_Loop':'Kiss',
'_K_Touch':'Kiss start',
'_M_Idle':'Breast grope idle',
'_M_Loop':'Breast grope',
'_M_Touch':'Breast grope start',
'_MLoop1': 'Masturbate',
'_MLoop2': 'Masturbate2',
'_Orgasm-': 'Climax-',
'_Orgasm_A':'Climax end',
'_Orgasm_B':'Climax end',
'_Orgasm_Loop':'Climax',
'_Orgasm_Start':'Climax start',
'_S_Idle':'Butt grope idle',
'_S_Loop':'Butt grope',
'_S_Touch':'Butt grope start',
'_Back_Dislikes':'Embarrassed back',
'_Front_Dislikes':'Embarrassed front',
'_Oral_Idle_IN': 'Cum in mouth start',
'_Oral_Idle': 'Cum in mouth',
'_Stop_Idle': 'Idling stop',
'_InsertIdle': 'Insert idle',
'_Idle-': 'Idling-',
'_Insert-': 'Insert-',
'_M_IN_Start':'Climax inside start',
'_SF_IN_Start':'Climax inside start',
'_A_SS_IN_Start':'Climax inside start',
'_SS_IN_Start':'Climax inside start',
'_A_WF_IN_Start':'Climax inside start',
'_A_WS_IN_Start':'Climax inside start',
'_WF_IN_Start':'Climax inside start',
'_A_M_IN_Start':'Climax inside start',
'_WS_IN_Start':'Climax inside start',
'_IN_Start':'Climax inside start',
'_M_IN_Loop':'Climax inside',
'_SF_IN_Loop':'Climax inside',
'_SS_IN_Loop':'Climax inside',
'_WF_IN_Loop':'Climax inside',
'_WS_IN_Loop':'Climax inside',
'_IN_Loop':'Climax inside',
'_A_IN_A':'Climax inside end',
'_SS_IN_A':'Climax inside end',
'_A_WS_IN_A':'Climax inside end',
'_WS_IN_A':'Climax inside end',
'_IN_A':'Climax inside end',
'_Pull':'Pull out',
'_OUT_Start':'Climax outside start',
'_M_OUT_Loop':'Climax outside',
'_OUT_Loop':'Climax outside',
'_OUT_A':'Climax outside end',
'_OLoop': 'Loop',
'_SLoop':'Fast loop',
'_WLoop':'Slow loop',
'_Drink_IN':'Swallow cum start',
'_Drink_A':'Swallow cum end',
'_Drink':'Swallow cum',
'_Vomit_IN': 'Spit out cum start',
'_Vomit_A': 'Spit out cum end',
'_Vomit': 'Spit out cum',
}
for item in translation_dict_normal:
name = name.replace(item, translation_dict_normal[item])
h_dict_used = False
for item in translation_dict_h:
if item in name:
name = name[1:] #add the description onto it because h animations don't have names, remove the S at the beginning of the name because all of them start with that
name = name.replace(item, translation_dict_h[item])
h_dict_used = True
break
action = rigify_armature.animation_data.action
rigify_armature.asset_mark()
action.asset_mark()
action.use_fake_user = True
my_areas = bpy.context.workspace.screens[0].areas
for area in my_areas:
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_object_viewport_armature = False
my_areas = bpy.context.workspace.screens[0].areas
for area in my_areas:
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_object_viewport_armature = True
action.name = os.path.basename(str(name))
action.asset_data.tags.new(file)
if h_dict_used:
action.asset_data.tags.new('NSFW')
action.asset_data.description = file
#delete the imported action, object and armature
bpy.data.actions.remove(bpy.data.actions[current_action_name])
imported_armature_armaturename = imported_armature.data.name
bpy.data.objects.remove(imported_armature)
try:
bpy.data.objects.remove(bpy.data.objects['Beta_Joints'])
bpy.data.objects.remove(bpy.data.objects['Beta_Surface'])
except:
#oh well
pass
bpy.data.armatures.remove(bpy.data.armatures[imported_armature_armaturename])
print(file)
print(str(time.time() - start))
c.toggle_console() #close console
class anim_import(bpy.types.Operator):
bl_idname = "kkbp.importanimation"
bl_label = "Import .fbx animation file"
bl_description = t('single_animation_tt')
bl_options = {'REGISTER', 'UNDO'}
filepath : bpy.props.StringProperty(maxlen=1024, default='', options={'HIDDEN'})
filter_glob : bpy.props.StringProperty(default='*.fbx', options={'HIDDEN'})
def execute(self, context):
main(self.filepath)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}