Files
KKBP_Importer/extras/createmapassetlibrary.py
小煜 5d9e09e9c3 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
2025-12-06 15:26:19 +08:00

355 lines
14 KiB
Python

#This will take a folder full of map files ripped from the game and create a blender asset library from them
# Usage instructions:
# Export koikatsu fbx animation files using https://www.youtube.com/watch?v=PeryYTsAN6E
# put all the map folders you exported into one folder
# any subfolder names in that folder will be used to tag them
# (i.e. /Desktop/all maps/map1/map1 fbx files + textures, then /Desktop/all maps/map2/map2 fbx files + textures, etc )
# Run this button from the kkbp extras section of the panel
# You can also do it in small batches and rotate out the already imported fbx files for new ones
# Or you can put a large list and kill blender when you want to pause the import process (it will resume from where it left off next time you run the script)
# remember to save the library file when it's done
import bpy, os, time, pathlib
from bpy.props import StringProperty
from .importstudio import import_studio_objects
from .. import common as c
from ..importing.modifymaterial import modify_material
from ..interface.dictionary_en import t
def better_fbx_map_import(directory):
already_loaded_images = [image.name for image in bpy.data.images]
#import
path = pathlib.Path(directory).rglob('*')
obj_list = []
for item in path:
if '.fbx' in str(item):
obj_list.append(str(item))
for obj in obj_list:
bpy.ops.better_import.fbx(filepath = obj, my_scale = 1.0)
#if nothing was imported, skip
if len(bpy.data.objects) < 2:
return
#delete duplicate objects and sky mesh
objs_to_delete = [obj for obj in bpy.data.objects if '.001' in obj.name or '.002' in obj.name or '_koi_sky_' in obj.name]
for obj in objs_to_delete:
data_name = obj.data.name
bpy.data.objects.remove(obj)
bpy.data.meshes.remove(bpy.data.meshes[data_name])
#remove all unused slots
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = bpy.data.objects[1]
bpy.ops.object.material_slot_remove_unused()
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
#setup simple toon shader for all objects
for obj in [mesh for mesh in bpy.data.objects if mesh.type == 'MESH' and mesh.material_slots[0].material]:
#if the material already exists, use that, else create it
try:
obj.material_slots[0].material = bpy.data.materials['KK ' + obj.material_slots[0].material.name]
c.kklog('Material already exists: ' + obj.material_slots[0].material.name)
except:
try:
template = bpy.data.materials['KK Simple'].copy()
except:
script_dir=pathlib.Path(__file__).parent.parent
template_path=(script_dir / '../KK Shader V8.0.blend').resolve()
filepath = str(template_path)
innerpath = 'Material'
templateList = ['KK Simple']
for template in templateList:
bpy.ops.wm.append(
filepath=os.path.join(filepath, innerpath, template),
directory=os.path.join(filepath, innerpath),
filename=template,
set_fake=False
)
template = bpy.data.materials['KK Simple'].copy()
if obj.material_slots[0].material:
template.name = 'KK ' + obj.material_slots[0].material.name
new_node = template.node_tree.nodes['Gentex'].node_tree.copy()
template.node_tree.nodes['Gentex'].node_tree = new_node
new_node.name = template.name
main_image_link = [node for node in obj.material_slots[0].material.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'][0].inputs[0]
if main_image_link.is_linked:
main_image = main_image_link.links[0].from_node.image
#replace the dds image with a png version
new_path = main_image.filepath.replace(".dds", ".png").replace(".DDS", ".png")
new_image_name = main_image.name.replace(".dds", ".png").replace(".DDS", ".png")
main_image.save_render(bpy.path.abspath(new_path))
bpy.data.images.load(filepath=bpy.path.abspath(new_path))
bpy.data.images[new_image_name].pack()
#create darktex
bpy.context.scene.kkbp.import_dir = os.path.dirname(bpy.data.filepath) + '\\'
main_image = bpy.data.images[new_image_name]
dark_image = modify_material.create_darktex(main_image, [.764, .880, 1])
bpy.context.scene.kkbp.import_dir = ''
#saturate both with color code
lut_light = 'Lut_TimeDay.png'
lut_dark = 'Lut_TimeDay.png'
modify_material.load_luts(lut_light, lut_dark)
for image in [main_image, dark_image]:
print('converting ' + image.name)
image.save()
image.reload()
image.colorspace_settings.name = 'sRGB'
modify_material.image_to_KK(image, lut_light) #run twice because of bug
new_pixels, width, height = modify_material.image_to_KK(image, lut_light)
image.pixels = new_pixels
#then load it in
new_node.nodes['light'].image = main_image
new_node.nodes['dark'].image = dark_image
norm_image_link = [node for node in obj.material_slots[0].material.node_tree.nodes if node.type == 'BSDF_PRINCIPLED'][0].inputs[22]
if norm_image_link.is_linked:
norm_image = norm_image_link.links[0].from_node.inputs[2].links[0].from_node.image
new_node.nodes['norm'].image == norm_image
obj.material_slots[0].material = template
def main(folder):
#Use the Better FBX importer addon if installed, else fallback to internal fbx importer
use_better_fbx_importer = 'better_fbx' in [addon.module for addon in bpy.context.preferences.addons]
c.toggle_console()
start = time.time()
#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])
#Set the view transform
bpy.context.scene.view_settings.view_transform = 'Standard'
#import the fbx files
fbx_folders = [(folder + sub) for sub in os.listdir(folder) if '.blend' not in sub]
print(fbx_folders)
for map in fbx_folders:
category = map.replace(folder, '')
filename = map
blend_filepath = map.replace(category,'') + category + '.blend'
#if a .blend file already exists, don't process this fbx file
if os.path.exists(blend_filepath):
continue
collection = bpy.data.collections.new(category)
bpy.context.scene.collection.children.link(collection)
layer_collection = bpy.context.view_layer.layer_collection
#Recursivly transverse layer_collection for a particular name
def recurLayerCollection(layerColl, collName):
found = None
if (layerColl.name == collName):
return layerColl
for layer in layerColl.children:
found = recurLayerCollection(layer, collName)
if found:
return found
layerColl = recurLayerCollection(layer_collection, collection.name)
bpy.context.view_layer.active_layer_collection = layerColl
#import map object based on Better FBX availability
if use_better_fbx_importer:
better_fbx_map_import(map)
else:
import_studio_objects(map)
#skip if this was an empty folder
if len(bpy.data.objects) < 2:
bpy.data.collections.remove(collection)
continue
#apply all armature transforms
arm = bpy.data.objects['Armature']
bpy.ops.object.select_all(action='SELECT')
bpy.context.view_layer.objects.active = arm
bpy.ops.object.transform_apply(location=True, rotation=True, scale=False)
#delete armature because the asset browser sucks
bpy.data.objects.remove(arm)
bpy.context.view_layer.objects.active = bpy.data.objects[1]
bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='MEDIAN')
#mark the collection as an asset and mark each object as an asset as long as it's not a wall, floor, ceiling or sky mesh
collection.asset_mark()
collection.asset_data.description = filename
collection.asset_data.tags.new(filename)
collection.asset_data.tags.new(category)
reject_list = [
'_yuka',
'_wall',
'_kabe',
'_ten',
'_black',
'_yuka',
'Armature',
'_sotokabe',
'_yane',
'_hana',
'_kuki',
'_kusa',
'_enkei',
'fensu',
'gura',
'_kage',
'_kmichi',
'_kmichi',
'_ie0',
'_seimon',
'_shadow',
'hana_0',
'kuki_0',
'kusa_0',
'_kyusui',
'_saku',
'_kumo',
'_kaidan',
'_nav',
'nob',
'_bushitu',
'map20_kk00',
'_styuka00',
'_ueki0',
'_kousya',
'_kanten',
'map21_light',
'sky_hoshi',
'_over',
'_suido',
'_kagamiwaku',
'o_koi_map100h_00_0',
'o_koi_map100_shawer00_00_0',
'_kar0',
'm00_o_box1__0',
'_reef_tga',
'_sky_star',
'_o_tree',
'o_ikegaki_0',
'i_stu_kiha',
'kouen_kakiwari_',
'kouen_road',
'kouen_pole',
'kouen_sibahu',
'mob_swimclub_',
'map34_kk0',
'p34_pool',
'ap36_kikai',
'_shitugaiki0',
'_tatemono',
'o_koi_map36_03_0',
'i_mizukri',
'koi_map17_kk00',
'_karten',
'i_gabyou',
'_pos0',
'_highyuka',
'map70_mob',
'_tesuri',
'o_soto_doa',
'o_soto_glass',
'o_soto_mado',
'o_koi_map80_00_s_0',
]
for obj in collection.all_objects:
mark_it = True
for item in reject_list:
if item in obj.name:
mark_it = False
break
if mark_it:
#apply transform then mark as asset
#bpy.ops.object.select_all(action='DESELECT')
#obj.select_set(True)
#bpy.context.view_layer.objects.active = obj
#bpy.ops.object.transform_apply(location=True, rotation=True, scale=False)
obj.asset_mark()
obj.asset_data.description = filename
obj.asset_data.tags.new(obj.name)
obj.asset_data.tags.new(category)
#hide the collection afterwards
bpy.context.scene.view_layers[0].active_layer_collection.exclude = True
#load a script into the layout tab
# if bpy.data.screens.get('Layout'):
# for area in bpy.data.screens['Layout'].areas:
# if area.type == 'DOPESHEET_EDITOR':
# area.ui_type = 'TEXT_EDITOR'
# area.spaces[0].text = bpy.data.texts.new(name='Generate previews, save and close')
# area.spaces[0].text.write(
# '''#stolen script to generate thumbnails for all objects
# import bpy, functools, time
# assets = [o for o in bpy.data.objects if o.asset_data] # Select all object assets
# assets.extend([m for m in bpy.data.materials if m.asset_data]) # Select all material assets
# for asset in assets:
# asset.asset_generate_preview()
# while bpy.app.is_job_running('RENDER_PREVIEW') == True:
# time.sleep(5)
# bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath)
# time.sleep(3)
# bpy.ops.wm.quit_blender()
# ''')
print(blend_filepath)
bpy.ops.wm.save_as_mainfile(filepath = blend_filepath)
bpy.data.collections.remove(collection)
for block in bpy.data.objects:
bpy.data.objects.remove(block)
for block in bpy.data.meshes:
bpy.data.meshes.remove(block)
for block in bpy.data.materials:
bpy.data.materials.remove(block)
for block in bpy.data.armatures:
bpy.data.armatures.remove(block)
for block in bpy.data.images:
bpy.data.images.remove(block)
for block in bpy.data.node_groups:
bpy.data.node_groups.remove(block)
print(str(time.time() - start))
c.toggle_console()
class map_asset_lib(bpy.types.Operator):
bl_idname = "kkbp.createmapassetlib"
bl_label = "Create map asset library"
bl_description = t('map_library_tt')
bl_options = {'REGISTER', 'UNDO'}
directory : StringProperty(maxlen=1024, default='', subtype='FILE_PATH', options={'HIDDEN'})
filter_glob : StringProperty(default='', options={'HIDDEN'})
data = None
mats_uv = None
structure = None
def execute(self, context):
main(self.directory)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
if __name__ == "__main__":
bpy.utils.register_class(map_asset_lib)
# test call
print((bpy.ops.kkbp.createmapassetlib('INVOKE_DEFAULT')))