- 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
658 lines
34 KiB
Python
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"}
|
|
|