# 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"}