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

165
importing/importbuttons.py Normal file
View File

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