Files
KKBP_Importer/exporting/bakematerials.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

658 lines
34 KiB
Python

# BAKE MATERIAL TO TEXTURE SCRIPT
# Bakes all materials of an object into image textures (to use in other programs)
# Will only bake a material if an image node is present in the green texture group
# If no image is present, a low resolution failsafe image will be baked to account for
# fully opaque or transparent materials that don't rely on a texture file
# If there are multiple image resolutions, only the highest resolution be baked
# Materials are baked to an 8-bit PNG with an alpha channel.
# (optional) Creates a copy of the model and generates a material atlas to the copy
# Notes:
# - This script deletes all camera objects in the scene
# - fillerplane driver + shader code taken from https://blenderartists.org/t/scripts-create-camera-image-plane/580839
# - material combiner code taken from https://github.com/Grim-es/material-combiner-addon/
import bpy, os, traceback, time, pathlib, subprocess
from .. import common as c
from ..interface.dictionary_en import t
def print_memory_usage(stri):
run = subprocess.run('wmic OS get FreePhysicalMemory', capture_output=True)
c.kklog((stri, '\n mem usage ', 16000 - int(run.stdout.split(b'\r')[2].split(b'\n')[1])/1000))
#setup and return a camera
def setup_camera():
#Delete all cameras in the scene
for obj in bpy.context.scene.objects:
if obj.type == 'CAMERA':
obj.select_set(True)
else:
obj.select_set(False)
bpy.ops.object.delete()
for block in bpy.data.cameras:
if block.users == 0:
bpy.data.cameras.remove(block)
#Add a new camera
bpy.ops.object.camera_add(enter_editmode=False, align='VIEW', location=(0, 0, 1), rotation=(0, 0, 0))
#save it for later
camera = bpy.context.active_object
#and set it as the active one
bpy.context.scene.camera=camera
#Set camera to orthographic
bpy.data.cameras[camera.name].type='ORTHO'
bpy.data.cameras[camera.name].ortho_scale=6
bpy.context.scene.render.pixel_aspect_y=1
bpy.context.scene.render.pixel_aspect_x=1
return camera
def setup_geometry_nodes_and_fillerplane(camera: bpy.types.Object):
object_to_bake = bpy.context.active_object
#create fillerplane
bpy.ops.mesh.primitive_plane_add()
bpy.ops.object.material_slot_add()
fillerplane = bpy.context.active_object
fillerplane.data.uv_layers[0].name = 'uv_main'
fillerplane.name = "fillerplane"
bpy.ops.object.editmode_toggle()
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.transform.resize(value=(0.5,0.5,0.5))
bpy.ops.uv.reset()
bpy.ops.object.editmode_toggle()
fillerplane.location = (0,0,-0.0001)
def setup_driver_variables(driver, camera):
cam_ortho_scale = driver.variables.new()
cam_ortho_scale.name = 'cOS'
cam_ortho_scale.type = 'SINGLE_PROP'
cam_ortho_scale.targets[0].id_type = 'CAMERA'
cam_ortho_scale.targets[0].id = bpy.data.cameras[camera.name]
cam_ortho_scale.targets[0].data_path = 'ortho_scale'
resolution_x = driver.variables.new()
resolution_x.name = 'r_x'
resolution_x.type = 'SINGLE_PROP'
resolution_x.targets[0].id_type = 'SCENE'
resolution_x.targets[0].id = bpy.context.scene
resolution_x.targets[0].data_path = 'render.resolution_x'
resolution_y = driver.variables.new()
resolution_y.name = 'r_y'
resolution_y.type = 'SINGLE_PROP'
resolution_y.targets[0].id_type = 'SCENE'
resolution_y.targets[0].id = bpy.context.scene
resolution_y.targets[0].data_path = 'render.resolution_y'
#setup X scale for bake object and plane
driver = object_to_bake.driver_add('scale',0).driver
driver.type = 'SCRIPTED'
setup_driver_variables(driver, camera)
driver.expression = "((r_x)/(r_y)*(cOS)) if (((r_x)/(r_y)) < 1) else (cOS)"
driver = fillerplane.driver_add('scale',0).driver
driver.type = 'SCRIPTED'
setup_driver_variables(driver, camera)
driver.expression = "((r_x)/(r_y)*(cOS)) if (((r_x)/(r_y)) < 1) else (cOS)"
#setup drivers for object's Y scale
driver = object_to_bake.driver_add('scale',1).driver
driver.type = 'SCRIPTED'
setup_driver_variables(driver, camera)
driver.expression = "((r_y)/(r_x)*(cOS)) if (((r_y)/(r_x)) < 1) else (cOS)"
driver = fillerplane.driver_add('scale',1).driver
driver.type = 'SCRIPTED'
setup_driver_variables(driver, camera)
driver.expression = "((r_y)/(r_x)*(cOS)) if (((r_y)/(r_x)) < 1) else (cOS)"
###########################
#import the premade flattener node to unwrap the mesh into the UV structure
c.import_from_library_file('NodeTree', ['.Geometry Nodes'])
#give the object a geometry node modifier
geonodes_mod = object_to_bake.modifiers.new('Flattener', 'NODES')
geonodes_mod.node_group = bpy.data.node_groups['.Geometry Nodes']
identifier = [str(i) for i in geonodes_mod.keys()][0]
geonodes_mod[identifier+'_attribute_name'] = 'uv_main'
geonodes_mod[identifier+'_use_attribute'] = True
#Make the originally selected object active again
c.switch(object_to_bake, 'OBJECT')
##############################
#Changes the material of the image plane to the material of the object,
# and then puts a render of the image plane into the specified folder
def sanitizeMaterialName(text: str) -> str:
'''Mat names need to be sanitized else you can't delete the files with windows explorer'''
for ch in ['\\','`','*','<','>','.',':','?','|','/','\"']:
if ch in text:
text = text.replace(ch,'')
return text
def bake_pass(folderpath: str, bake_type: str):
'''Folds the body / clothes / hair down to a UV rectangle
Places a filler plane right below it to fill in the rest of the image
Bakes all materials on this object down to an image using the orthographic camera
'''
#get the currently selected object as the active object
object_to_bake = bpy.context.active_object
#remember what order the materials are in for later
original_material_order = []
for matslot in object_to_bake.material_slots:
original_material_order.append(matslot.name)
#if this is a light or dark pass, make sure the color output is a constant light or dark
combine = bpy.data.node_groups['.Combine colors']
combine.links.remove(combine.nodes['mix'].inputs[0].links[0])
combine.nodes['mix'].inputs[0].default_value = 1 if bake_type == 'light' else 0
#go through each material slot
for index, current_material in enumerate(object_to_bake.data.materials):
#Don't bake this material if it doesn't have the bake tag
if not current_material.get('bake'):
c.kklog(f'Detected material that cannot be finalized. Skipping: {current_material.name}')
continue
nodes = current_material.node_tree.nodes
links = current_material.node_tree.links
#Turn off the normals for the toon_shading shading node group input if this isn't a normal pass
if nodes.get('textures') and bake_type != 'normal':
toon_shading = nodes.get('textures').node_tree.nodes.get('shade')
if toon_shading:
original_normal_state = toon_shading.inputs[1].default_value
toon_shading.inputs[1].default_value = 0
#if this is a normal pass, attach the normal passthrough to the output
elif nodes.get('textures') and bake_type == 'normal':
if len(nodes['textures'].outputs):
links.remove(nodes['out'].inputs[0].links[0])
links.new(nodes['textures'].outputs[-1], nodes['out'].inputs[0])
if nodes.get('textures'):
#Go through each of the textures loaded into the textures group and get the highest resolution one
highest_resolution = [0, 0]
for image_node in nodes['textures'].node_tree.nodes:
if image_node.type == 'TEX_IMAGE' and image_node.image:
image_size = image_node.image.size[0] * image_node.image.size[1]
largest_so_far = highest_resolution[0] * highest_resolution[1]
if image_size > largest_so_far:
highest_resolution = image_node.image.size
resolution_multiplier = bpy.context.scene.kkbp.bake_mult
#Render an image using the highest dimensions
if highest_resolution:
bpy.context.scene.render.resolution_x=highest_resolution[0] * resolution_multiplier
bpy.context.scene.render.resolution_y=highest_resolution[1] * resolution_multiplier
else:
#if no images were found, render a 64px failsafe image anyway to catch
# materials that are a solid color, don't rely on textures, or are completely transparent
bpy.context.scene.render.resolution_x=64
bpy.context.scene.render.resolution_y=64
#set every material slot except the current material to be transparent
for matslot in object_to_bake.material_slots:
if matslot.material != current_material:
matslot.material = bpy.data.materials['KK Eyeline kage ' + c.get_name()]
#set the filler plane to the current material
bpy.data.objects['fillerplane'].material_slots[0].material = current_material
#then render it
matname = sanitizeMaterialName(current_material.name)
matname = matname[:-4] if matname[-4:] == '-ORG' else matname
bpy.context.scene.render.filepath = folderpath + matname + ' ' + bake_type
bpy.context.scene.render.image_settings.file_format='PNG'
bpy.context.scene.render.image_settings.color_mode='RGBA'
print('Rendering {} / {}'.format(index+1, len(object_to_bake.data.materials)))
bpy.ops.render.render(write_still = True)
#reset folderpath after render
bpy.context.scene.render.filepath = folderpath
#Restore the value in the toon_shading shading node group for the normals
if nodes.get('textures') and bake_type != 'normal':
toon_shading = nodes.get('textures').node_tree.nodes.get('shade')
if toon_shading:
toon_shading.inputs[1].default_value = original_normal_state
#Restore the links if they were edited for the normal pass
elif nodes.get('textures') and bake_type == 'normal':
if len(nodes['textures'].outputs):
links.remove(nodes['out'].inputs[0].links[0])
links.new(nodes['combine'].outputs[0], nodes['out'].inputs[0])
#reset material slots to their original order
for material_index in range(len(original_material_order)):
object_to_bake.material_slots[material_index].material = bpy.data.materials[original_material_order[material_index]]
#reset the color output group link
combine = bpy.data.node_groups['.Combine colors']
combine.links.new(combine.nodes['input'].outputs[2], combine.nodes['mix'].inputs[0])
def cleanup():
# Deselect all objects
bpy.ops.object.select_all(action='DESELECT')
#Select the camera
for camera in [o for o in bpy.data.objects if o.type == 'CAMERA']:
bpy.data.objects.remove(camera)
#Select fillerplane
for fillerplane in [o for o in bpy.data.objects if 'fillerplane' in o.name]:
bpy.data.objects.remove(fillerplane)
#delete orphan data
for block in bpy.data.meshes:
if block.users == 0:
bpy.data.meshes.remove(block)
for block in bpy.data.cameras:
if block.users == 0:
bpy.data.cameras.remove(block)
for ob in [obj for obj in bpy.context.view_layer.objects if obj and obj.type == 'MESH']:
#delete the geometry modifier
if ob.modifiers.get('Flattener'):
ob.modifiers.remove(ob.modifiers['Flattener'])
#delete the two scale drivers
ob.animation_data.drivers.remove(ob.animation_data.drivers[0])
ob.animation_data.drivers.remove(ob.animation_data.drivers[0])
ob.scale = (1,1,1)
bpy.data.node_groups.remove(bpy.data.node_groups['.Geometry Nodes'])
def replace_all_baked_materials(folderpath: str, bake_object: bpy.types.Object):
#load all baked images into blender
fileList = pathlib.Path(folderpath).glob('*.png')
files = [file for file in fileList if file.is_file()]
for file in files:
try:
image = bpy.data.images.load(filepath=str(file))
image.pack()
#if there was an older version of this image, get rid of it
if image.name[-4:] == '.001':
if bpy.data.images.get(image.name[:-4]):
bpy.data.images[image.name[:-4]].user_remap(image)
bpy.data.images.remove(bpy.data.images[image.name[:-4]])
image.name = image.name[:-4]
except:
c.kklog(f'Could not load in file because the name exceeds 64 characters: {file}')
#now all needed images are loaded into the file. Match each material to it's image textures
for bake_type in ['light', 'dark', 'normal']:
for mat in bake_object.material_slots:
image = bpy.data.images.get(mat.material.name.replace('-ORG', '') + f' {bake_type}.png', '')
if image:
#the simplified material already exists and is loaded into the material slot, so just load in the image
if mat.material.get('simple'):
simple = mat.material
textures_group = simple.node_tree.nodes['textures'].node_tree
textures_group.nodes[bake_type].image = image
#the simplified material already exists, but the user swapped it back to the -ORG version to rebake it,
# so load the material back into the material slot and load in the image
elif mat.material.get('bake') and '-ORG' in mat.material.name and bpy.data.materials.get(mat.material.name.replace('-ORG','')):
simple = bpy.data.materials[mat.material.name.replace('-ORG','')]
mat.material = simple
textures_group = simple.node_tree.nodes['textures'].node_tree
print(mat.material.name)
textures_group.nodes[bake_type].image = image
#check if a simplified version of this material exists yet. If it doesn't, create it
elif mat.material.get('bake'):
#rename the original material to "material_name-ORG" and create the simplified material
mat.material.name += '-ORG'
try:
simple = bpy.data.materials['KK Simple'].copy()
except:
c.import_from_library_file('Material', ['KK Simple'], use_fake_user = False)
simple = bpy.data.materials['KK Simple'].copy()
simple.name = mat.material.name.replace('-ORG', '')
textures_group = simple.node_tree.nodes['textures'].node_tree.copy()
textures_group.name = simple.name
simple.node_tree.nodes['textures'].node_tree = textures_group
textures_group.nodes[bake_type].image = image
# you have the ability to only bake the light textures, but it looks weird if there is no dark texture to go along with it,
# put the light image into the dark slot. it will be overwritten if the dark texture exists on the next loop
if bake_type == 'light':
textures_group.nodes['dark'].image = image
#and then replace the original material with this new simplified one
mat.material.use_fake_user = True
def replace_mat():
if bpy.app.version[0] > 3:
blend_method = mat.material.surface_render_method
mat.material = simple
mat.material.surface_render_method = blend_method
mat.material.use_transparency_overlap = True if ('KK Eyewhites (sirome) ' + c.get_name() in mat.name) else False
else:
blend_method = mat.material.blend_method
mat.material = simple
mat.material.blend_method = blend_method
mat.material.show_transparent_back = False
simple['simple'] = True
replace_mat()
#load the Eevee Mod simple shader if using Eevee Mod
if bpy.context.scene.kkbp.shader_dropdown == 'C':
try:
simple = bpy.data.node_groups['.Simple Shader (Eevee Mod)'].copy()
except:
c.import_from_library_file('NodeTree', ['.Simple Shader (Eevee Mod)'], use_fake_user = False)
simple = bpy.data.node_groups['.Simple Shader (Eevee Mod)'].copy()
#and then replace the original material with this new simplified one
mat.material.use_fake_user = True
replace_mat()
def create_material_atlas(folderpath: str):
'''Merges all the finalized material png files into a single atlas file, copies the current model and applies the atlas to the copy'''
# https://blender.stackexchange.com/questions/127403/change-active-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
def remove_orphan_data():
#revert the image back from the atlas file to the baked file
for mat in bpy.data.materials:
if mat.name[-4:] == '-ORG':
simplified_name = mat.name[:-4]
if bpy.data.materials.get(simplified_name):
simplified_mat = bpy.data.materials[simplified_name]
for bake_type in ['light', 'dark', 'normal']:
simplified_mat.node_tree.nodes['textures'].node_tree.nodes[bake_type].image = bpy.data.images.get(simplified_name + ' ' + bake_type + '.png')
#delete orphan data
for cat in [bpy.data.armatures, bpy.data.objects, bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.node_groups]:
for block in cat:
if block.users == 0:
cat.remove(block)
if bpy.data.collections.get(c.get_name() + ' atlas'):
c.kklog(f'deleting previous collection "{c.get_name()} atlas" and regenerating atlas model...')
def del_collection(coll):
for c in coll.children:
del_collection(c)
bpy.data.collections.remove(coll,do_unlink=True)
del_collection(bpy.data.collections[c.get_name() + ' atlas'])
remove_orphan_data()
#show the original collection again
c.show_layer_collection(c.get_name(), False)
#Change the Active LayerCollection to the character collection
layer_collection = bpy.context.view_layer.layer_collection
layerColl = recurLayerCollection(layer_collection, c.get_name())
bpy.context.view_layer.active_layer_collection = layerColl
# https://blender.stackexchange.com/questions/157828/how-to-duplicate-a-certain-collection-using-python
from collections import defaultdict
def copy_objects(from_col, to_col, linked, dupe_lut):
for o in from_col.objects:
dupe = o.copy()
if not linked and o.data:
dupe.data = dupe.data.copy()
to_col.objects.link(dupe)
dupe_lut[o] = dupe
def copy(parent, collection, linked=False):
dupe_lut = defaultdict(lambda : None)
def _copy(parent, collection, linked=False):
cc = bpy.data.collections.new(collection.name)
copy_objects(collection, cc, linked, dupe_lut)
for c in collection.children:
_copy(cc, c, linked)
parent.children.link(cc)
return cc
the_copy = _copy(parent, collection, linked)
for o, dupe in tuple(dupe_lut.items()):
parent = dupe_lut[o.parent]
if parent:
dupe.parent = parent
return the_copy
context = bpy.context
scene = context.scene
col = context.collection
assert(col is not scene.collection)
copied_collection = copy(scene.collection, col)
copied_collection.name = c.get_name() + ' atlas'
#setup materials for the combiner script
for obj in [o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'MESH']:
for mat in [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
nodes = mat.node_tree.nodes
links = mat.node_tree.links
emissive_node = nodes.new('ShaderNodeEmission')
emissive_node.name = 'Emission'
image_node = nodes.new('ShaderNodeTexImage')
image_node.name = 'Image Texture'
links.new(emissive_node.inputs[0], image_node.outputs[0])
image_node.image = nodes['textures'].node_tree.nodes['light'].image
context.view_layer.objects.active = obj
bpy.ops.object.material_slot_remove_unused()
#call the material combiner script
bpy.ops.kkbp.combiner()
#replace all images with the atlas in a new atlas material
bake_types = []
if scene.kkbp.bake_light_bool:
bake_types.append('light')
if scene.kkbp.bake_dark_bool:
bake_types.append('dark')
if scene.kkbp.bake_norm_bool:
bake_types.append('normal')
for index, obj in enumerate([o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'MESH']):
#fix modifiers for all objects in this collection
for mod in obj.modifiers:
if mod.type == 'ARMATURE':
#fix the armature modifier to use the copied aramture
copied_armature = [o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'ARMATURE'][0]
mod.object = copied_armature
elif mod.type == 'SOLIDIFY':
#disable the outline on the atlased object because I don't feel like fixing it
obj.modifiers['Outline Modifier'].show_render = False
obj.modifiers['Outline Modifier'].show_viewport = False
elif mod.type == 'UV_WARP':
#fix the UV warp modifier to use the copied armature
copied_armature = [o for o in bpy.data.collections[c.get_name() + ' atlas'].all_objects if o.type == 'ARMATURE'][0]
mod.object_from = copied_armature
mod.object_to = copied_armature
#check if this object had any atlas-able materials to begin with. If not, skip
if not [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
continue
for bake_type in bake_types:
#check for atlas dupes
atlas_image_name = f'{sanitizeMaterialName(obj.name).replace("001","")}_{bake_type}.png'
if bpy.data.images.get(atlas_image_name):
bpy.data.images.remove(bpy.data.images.get(atlas_image_name))
#the atlas image is originally named after the index of the object. Rename it to the object name
original_image_path = os.path.join(context.scene.kkbp.import_dir, 'atlas_files', f'{index}_{bake_type}.png')
new_image_path = os.path.join(context.scene.kkbp.import_dir, 'atlas_files', atlas_image_name)
if os.path.exists(original_image_path):
try:
os.rename(original_image_path, new_image_path)
except:
#rename failed because the file already exists. Delete the old one and try again
os.remove(new_image_path)
os.rename(original_image_path, new_image_path)
#then load it into blender
atlas_image = bpy.data.images.load(new_image_path)
bpy.data.images.remove(bpy.data.images.get(f'{index}_{bake_type}.png'))
for material in [mat_slot.material for mat_slot in obj.material_slots if mat_slot.material.get('simple')]:
image = material.node_tree.nodes['textures'].node_tree.nodes[bake_type].image
if image:
if image.name == 'Template: Pattern Placeholder':
image = None
if not image:
print(image)
continue
else:
if not bpy.data.materials.get('{} Atlas'.format(material.name)):
#remove the emission nodes from earlier
if material.node_tree.nodes.get('Emission'):
material.node_tree.nodes.remove(material.node_tree.nodes['Image Texture'])
material.node_tree.nodes.remove(material.node_tree.nodes['Emission'])
atlas_material = material.copy()
atlas_material['simple'] = False
atlas_material['atlas'] = True
atlas_material.name = '{} Atlas'.format(material.name)
new_group = atlas_material.node_tree.nodes['textures'].node_tree.copy()
new_group.name = '{} Atlas'.format(material.name)
else:
atlas_material = bpy.data.materials.get('{} Atlas'.format(material.name))
new_group = bpy.data.node_groups.get('{} Atlas'.format(material.name))
atlas_material.node_tree.nodes['textures'].node_tree = new_group
new_group.nodes[bake_type].image = atlas_image
#load in the light image to the dark slot to make it look better when only the light colors are baked.
# This will be overwritten with the dark image in the next loop if the user baked it
if bake_type == 'light':
new_group.nodes['dark'].image = atlas_image
#replace all images with the atlas in a new atlas material
for mat_slot in [m for m in obj.material_slots if m.material.get('simple')]:
material = mat_slot.material
atlas_material = bpy.data.materials.get('{} Atlas'.format(material.name))
mat_slot.material = atlas_material
#setup the new collection for exporting
if bpy.app.version[0] > 3:
layer_collection = bpy.context.view_layer.layer_collection
layerColl = recurLayerCollection(layer_collection, c.get_name() + ' atlas')
bpy.context.view_layer.active_layer_collection = layerColl
bpy.ops.collection.exporter_add(name="IO_FH_fbx")
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.object_types = {'EMPTY', 'ARMATURE', 'MESH', 'OTHER'}
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.use_mesh_modifiers = False
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.add_leaf_bones = False
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.bake_anim = False
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.apply_scale_options = 'FBX_SCALE_ALL'
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.path_mode = 'COPY'
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.embed_textures = False
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.mesh_smooth_type = 'OFF'
bpy.data.collections[c.get_name() + ' atlas'].exporters[0].export_properties.filepath = os.path.join(folderpath.replace('baked_files', 'atlas_files'), f'{sanitizeMaterialName(c.get_name())} exported model atlas.fbx')
#hide the new collection
c.show_layer_collection('Bone Widgets', True)
c.show_layer_collection('Rigged tongue ' + c.get_name(), True)
c.show_layer_collection('Rigged tongue ' + c.get_name() + '.001', True)
c.show_layer_collection('Bone Widgets.001', True)
c.show_layer_collection(c.get_name() + ' atlas', True)
remove_orphan_data()
class bake_materials(bpy.types.Operator):
bl_idname = "kkbp.bakematerials"
bl_label = "Bake and generate atlased model"
bl_description = t('bake_mats_tt')
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
try:
#just use the pmx folder for the baked files
scene = context.scene.kkbp
folderpath = os.path.join(context.scene.kkbp.import_dir, 'baked_files', '')
last_step = time.time()
c.toggle_console()
c.reset_timer()
c.kklog('Switching to EEVEE for material baking...')
bpy.context.scene.render.engine = 'BLENDER_EEVEE_NEXT' if bpy.app.version[0] > 3 else 'BLENDER_EEVEE'
c.switch(c.get_body(), 'OBJECT')
c.set_viewport_shading('SOLID')
#enable transparency
bpy.context.scene.render.film_transparent = True
bpy.context.scene.render.filter_size = 0.50
for bake_object in c.get_all_bakeable_objects():
#do a quick check to make sure this object has any materials that can be baked
worth_baking = [m for m in bake_object.material_slots if m.material.get('bake')]
if not worth_baking:
c.kklog(f'Not finalizing object because there were no materials worth baking: {bake_object.name}')
continue
#make sure the collection for this object is enabled in the outliner if it is a clothing item
if bake_object != c.get_body():
original_collection_state = c.get_layer_collection_state(bake_object.users_collection[0].name)
c.show_layer_collection(bake_object.users_collection[0].name, False)
#hide all objects except this one
for obj in [o for o in bpy.context.view_layer.objects if o]:
obj.hide_render = True
#unhide the object to bake (but only if the old baking system is not used)
if not bpy.context.scene.kkbp.old_bake_bool:
bake_object.hide_render = False
camera = setup_camera()
c.switch(bake_object)
setup_geometry_nodes_and_fillerplane(camera)
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
#perform the baking operation
bake_types = []
if scene.bake_light_bool:
bake_types.append('light')
if scene.bake_dark_bool:
bake_types.append('dark')
if scene.bake_norm_bool:
bake_types.append('normal')
for bake_type in bake_types:
bake_pass(folderpath, bake_type)
cleanup()
#restore the original collection state
if bake_object != c.get_body():
c.show_layer_collection(bake_object.users_collection[0].name, original_collection_state)
#disable transparency
bpy.context.scene.render.film_transparent = False
bpy.context.scene.render.filter_size = 1.5
for bake_object in c.get_all_bakeable_objects():
replace_all_baked_materials(folderpath, bake_object)
#show all objects again
for obj in bpy.context.view_layer.objects:
obj.hide_render = False
if scene.use_atlas:
create_material_atlas(folderpath)
#setup the original collection for exporting
# https://blender.stackexchange.com/questions/127403/change-active-collection
#Recursively 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
layer_collection = bpy.context.view_layer.layer_collection
layerColl = recurLayerCollection(layer_collection, c.get_name())
bpy.context.view_layer.active_layer_collection = layerColl
if bpy.app.version[0] != 3:
if not bpy.data.collections[c.get_name()].exporters:
bpy.ops.collection.exporter_add(name="IO_FH_fbx")
bpy.data.collections[c.get_name()].exporters[0].export_properties.object_types = {'EMPTY', 'ARMATURE', 'MESH', 'OTHER'}
bpy.data.collections[c.get_name()].exporters[0].export_properties.use_mesh_modifiers = False
bpy.data.collections[c.get_name()].exporters[0].export_properties.add_leaf_bones = False
bpy.data.collections[c.get_name()].exporters[0].export_properties.bake_anim = False
bpy.data.collections[c.get_name()].exporters[0].export_properties.apply_scale_options = 'FBX_SCALE_ALL'
bpy.data.collections[c.get_name()].exporters[0].export_properties.path_mode = 'COPY'
bpy.data.collections[c.get_name()].exporters[0].export_properties.embed_textures = False
bpy.data.collections[c.get_name()].exporters[0].export_properties.mesh_smooth_type = 'OFF'
bpy.data.collections[c.get_name()].exporters[0].export_properties.filepath = os.path.join(folderpath.replace('baked_files', 'atlas_files'), f'{sanitizeMaterialName(c.get_name())} exported model.fbx')
c.toggle_console()
c.kklog('Finished in ' + str(time.time() - last_step)[0:4] + 's')
c.set_viewport_shading('SOLID')
return {'FINISHED'}
except:
c.kklog('Unknown python error occurred', type = 'error')
c.kklog(traceback.format_exc())
c.set_viewport_shading('SOLID')
self.report({'ERROR'}, traceback.format_exc())
return {"CANCELLED"}