import bpy from bpy.types import Operator from bpy.utils import register_class, unregister_class from bpy.props import StringProperty, BoolProperty, IntProperty from pathlib import Path import os import sys import re from .utils import SBConstants, get_base_folder_patho, select_only_this, select_only_these, get_texture_channels, blender_default_colorspace from .messages import print_message from .background_and_progress import BakeInProgress as Bip FILE_EXTENSIONS = {"PNG": "png", "JPEG": "jpg", "TIFF": "tiff", "TARGA": "tga","TARGA_RAW": "tga", "OPEN_EXR": "exr"} def apply_scene_col_settings(context, scene): scene.display_settings.display_device = context.scene.display_settings.display_device scene.view_settings.view_transform = context.scene.view_settings.view_transform scene.view_settings.look = context.scene.view_settings.look scene.view_settings.exposure = context.scene.view_settings.exposure scene.view_settings.gamma = context.scene.view_settings.gamma scene.sequencer_colorspace_settings.name = context.scene.sequencer_colorspace_settings.name return True class SimpleBake_OT_Save_Images_Externally(Operator): """Saved baked images externally""" bl_idname = "simplebake.save_images_externally" bl_description = "Save baked images externally" bl_label = "Save" bake_operation_id: StringProperty() bake_mode: StringProperty() lightmap_apply_colman: BoolProperty(default=False) def execute(self, context): sbp = context.scene.SimpleBake_Props #Set what we need from the panel self.file_format = sbp.export_file_format self.folder_per_object = sbp.export_folder_per_object self.apply_col_man_to_col = sbp.apply_col_man_to_col self.merged_bake = sbp.merged_bake self.merged_bake_name = sbp.merged_bake_name self.export_cycles_col_space = sbp.export_cycles_col_space self.in_background = True if "--background" in sys.argv or sbp.fake_background else False #Get all the images we want to save externally imgs = ([img for img in bpy.data.images if "SB_bake_operation_id" in img and img["SB_bake_operation_id"] == self.bake_operation_id and "SB_this_bake" in img and img["SB_this_bake"] == self.bake_mode ]) #Remove any already exported for img in list(imgs): exported_list = list(img["SB_exported_list"]) if self.bake_operation_id in exported_list: print_message(context, f"{img.name} has already been exported") imgs.remove(img) #Exit here if there aren't any if len(imgs) == 0: print_message(context, f"No images to export, exiting") return {'FINISHED'} #Find the save location for each image save_locations = {} if self.in_background: p = sbp.export_path.replace("//", "") base_save_folder = Path(sbp.base_folder_override) / p else: base_save_folder = Path(bpy.path.abspath(sbp.export_path)) print_message(context, f"Saving baked images to {str(base_save_folder)}") if not self.folder_per_object:#Not folder per object for img in imgs: save_locations[img.name] = base_save_folder #All the same else: #Folder per object if self.merged_bake: for img in imgs: save_locations[img.name] = base_save_folder / self.merged_bake_name #All the same else: for img in imgs: obj_name = img["SB_bake_object"] if img.get("SB_bake_material"): save_locations[img.name] = base_save_folder / obj_name / img["SB_bake_material"] else: save_locations[img.name] = base_save_folder / obj_name #Create the base save folder if it doesn't already exist if not os.path.isdir(str(base_save_folder)): os.makedirs(str(base_save_folder)) #We want a scene so we can control bit depth etc. s = bpy.data.scenes.new("SB_Export_Temp") s.render.image_settings.file_format = self.file_format #Do we want to apply colour management from current scene? #CyclesBake s.view_settings.view_transform = "Standard" #default if sbp.global_mode == "CyclesBake": if self.export_cycles_col_space and self.bake_mode != "NORMAL": print_message(context, "Applying scene col management settings CyclesBake") apply_scene_col_settings(context, s) else: print_message(context, f"No col management settings for {self.bake_mode}") #PBR else: if self.apply_col_man_to_col and self.bake_mode == SBConstants.PBR_DIFFUSE: print_message(context, "Applying scene col management settings to PBR Diffuse") apply_scene_col_settings(context, s) else: print_message(context, f"No col management settings for {self.bake_mode}") #Either if self.lightmap_apply_colman and self.bake_mode == SBConstants.LIGHTMAP: print_message(context, "Applying scene col management settings lightmap") apply_scene_col_settings(context, s) #Bit depth if self.file_format in ["JPEG", "TARGA", "TARGA_RAW"]: pass # Skip this entirely elif (sbp.everything_16bit or self.bake_mode == SBConstants.PBR_NORMAL and not sbp.no_force_32bit_normals or self.bake_mode == "NORMAL" and not sbp.no_force_32bit_normals): s.render.image_settings.color_depth = "16" elif self.file_format == "OPEN_EXR": s.render.image_settings.color_depth = "32" else: s.render.image_settings.color_depth = "8" #Channels if self.file_format == "JPEG": s.render.image_settings.color_mode = get_texture_channels(context, self.bake_mode) else: ua = sbp.texture_bg_col[3] < 1.0 if ua: s.render.image_settings.color_mode = "RGBA" else: s.render.image_settings.color_mode = get_texture_channels(context, self.bake_mode) print_message(context, f"Chosen {s.render.image_settings.color_mode} for {self.bake_mode}") #Extras if self.file_format == "JPEG": s.render.image_settings.quality = sbp.jpeg_quality if self.file_format == "OPEN_EXR": s.render.image_settings.exr_codec = sbp.exr_codec_export #Save to disk for img in imgs: save_folder = save_locations[img.name] #Create the folder in the file system if it's not there already create_dir_if_needed(save_folder) ext = FILE_EXTENSIONS[self.file_format] prefs = context.preferences.addons[__package__].preferences if prefs.capitalise_file_extensions: ext = ext.upper() #Some housekeeping to do if we are going to save a UDIM image udim_marker = "." if len(img.tiles)>1 else "" if len(img.tiles)>1: pattern = re.compile(rf"^{re.escape(img.name)}\.(\d{{4,}})\.{re.escape(ext)}$") for file in save_folder.iterdir(): if file.is_file() and pattern.match(file.name): print(f"Deleting: {file}") # optional file.unlink() save_path = save_folder / (img.name + "." + udim_marker + ext) print_message(context, f"Save path: {str(save_path)}") #Save image #Let's try this img.save_render(str(save_path).encode('utf-8').decode('utf-8'), scene=s) #Store full path. We may need it later on. About to update to relative path full_save_path = str(save_path) img["SB_full_save_path"] = full_save_path #Store the bit depth and channels too as this is oddly hard to get back later img["SB_bit_depth"] = s.render.image_settings.color_depth img["SB_channels"] = s.render.image_settings.color_mode img["SB_view_transform"] = s.view_settings.view_transform img["SB_exr_codec"] = s.render.image_settings.exr_codec #Some changes needed for UDIM images img.source = "TILED" if len(img.tiles)>1 else "FILE" #TRY TO covnert to relative path. Even if in the background, as the append op will fix this try: img.filepath = bpy.path.relpath(full_save_path) except ValueError: print_message(context, "Converting to relative path failed - probably different drive on Windows") img.filepath = full_save_path try: img.unpack(method="REMOVE") except: pass #Colour space settings for (what is now) external image if self.file_format == "OPEN_EXR": #Get default for non-col (for OpenEXR) img.colorspace_settings.name = blender_default_colorspace(float_buffer=False, col=False)[1] elif "SB_denoised" in img and img["SB_denoised"] and( sbp.global_mode in [SBConstants.CYCLESBAKE, SBConstants.CYCLESBAKE_S2A]): img.colorspace_settings.name = sbp.cyclesbake_cs else: pass # Keep the defaults from when image was created #Record that we exported this image for this bake job exported_list = list(img["SB_exported_list"]) exported_list.append(self.bake_operation_id) img["SB_exported_list"] = exported_list print_message(context, f"{img.name} saved externally") #When all done, remove the temp scene bpy.data.scenes.remove(s) return {'FINISHED'} def create_dir_if_needed(path): path = str(path) if not os.path.isdir(path): os.makedirs(path) class SimpleBake_OT_Save_Objects_Externally(Operator): """Saved baked objects externally""" bl_idname = "simplebake.save_objects_externally" bl_description = "Save baked objects externally" bl_label = "Save" bake_operation_id: StringProperty() def execute(self, context): sbp = context.scene.SimpleBake_Props if not Bip.Sequence.should_run_teardown(): print_message(context, "No external mesh save - sequence and not last run") return {'FINISHED'} #Nope out if this is part of a sequence #Set what we need from the panel self.apply_mods_on_mesh_export = sbp.apply_mods_on_mesh_export self.apply_transformation = sbp.apply_transformation self.copy_and_apply = sbp.copy_and_apply self.export_folder_per_object = sbp.export_folder_per_object self.merged_bake = sbp.merged_bake self.merged_bake_name = sbp.merged_bake_name self.in_background = True if "--background" in sys.argv or sbp.fake_background else False self.mesh_mode = sbp.export_mesh_individual_or_combined blend_name = os.path.splitext(os.path.basename(bpy.data.filepath))[0] mesh_export_name = sbp.mesh_export_name if sbp.mesh_export_name != "%blend_file_name%" else blend_name #Find objects we want to export. Depends what we are actually doing: if sbp.apply_bakes_to_original and (sbp.cycles_s2a or sbp.selected_s2a): if sbp.cycles_s2a: objs = [sbp.targetobj_cycles] if sbp.selected_s2a: objs = [sbp.targetobj] elif sbp.apply_bakes_to_original: objs = [i.obj_point for i in sbp.objects_list] else: objs = ([o for o in context.scene.objects if "SB_copy_and_apply_from" in o and "SB_bake_operation_id" in o and o["SB_bake_operation_id"] == self.bake_operation_id]) #Maybe merge the objects if sbp.merge_export_obj and sbp.export_mesh_individual_or_combined == "combined" and sbp.merged_bake: print_message(context, "Merging baked objects within the output mesh file") dup_objects_names = [] for o in objs: select_only_this(context, o) bpy.ops.object.duplicate_move() new_obj = bpy.context.active_object dup_objects_names.append(new_obj.name) #Join them together select_only_these(context, [o:=bpy.data.objects.get(o_name) for o_name in dup_objects_names if o!=None]) bpy.ops.object.join() #Just the one object now objs = [context.active_object] #Clear all materials except the first one o = context.active_object i=0 for slot in o.material_slots: if i != 0: slot.material = None i+=1 #Name the mesh context.active_object.name = f"{mesh_export_name}_Baked" #Keep track of this for later deletion merged_mesh_obj_name = context.active_object.name #Export if self.in_background: p = sbp.export_path.replace("//", "") base_save_folder = Path(sbp.base_folder_override) / p else: base_save_folder = Path(bpy.path.abspath(sbp.export_path)) print_message(context, f"Saving exported mesh to {str(base_save_folder)}") #Create the base save folder if it doesn't already exist create_dir_if_needed(base_save_folder) if sbp.export_format == "fbx": operator = getattr(bpy.ops.export_scene, "fbx") extension = ".fbx" #preset_path = bpy.utils.preset_paths('operator/export_scene.fbx/') use_selection_name = "use_selection" myargs = { 'check_existing': False, 'use_selection': True, 'use_mesh_modifiers': self.apply_mods_on_mesh_export, 'bake_space_transform': self.apply_transformation, 'path_mode': "STRIP" } elif sbp.export_format == "obj": operator = getattr(bpy.ops.wm, "obj_export") extension = ".obj" #preset_path = bpy.utils.preset_paths('operator/wm.obj_export/') use_selection_name = "export_selected_objects" myargs = { 'check_existing': False, 'export_selected_objects': True, 'apply_modifiers': self.apply_mods_on_mesh_export, 'path_mode': "STRIP" } elif sbp.export_format == "dae": operator = getattr(bpy.ops.wm, "collada_export") extension = ".dae" #preset_path = bpy.utils.preset_paths('operator/wm.obj_export/') use_selection_name = "selected" myargs = { 'check_existing': False, 'selected': True, 'apply_modifiers': self.apply_mods_on_mesh_export, 'use_texture_copies': False } else: operator = getattr(bpy.ops.export_scene, "gltf") extension = ".glb" preset_path = bpy.utils.preset_paths('operator/export_scene.gltf/') use_selection_name = "use_selection" myargs = { 'check_existing': False, 'use_selection': True, 'export_apply': self.apply_mods_on_mesh_export } kwargs = None #If we are using a preset, gather those settings if (preset_name:=sbp.export_mesh_preset_name) not in ["None",""]: filepath = sbp.export_mesh_preset_name #Internal name is the path to the preset file class Container(object): __slots__ = ('__dict__',) op = Container() file = open(filepath, 'r') # storing the values from the preset on the class for line in file.readlines()[3::]: exec(line, globals(), locals()) kwargs = op.__dict__ def do_export(filepath): if kwargs: print_message(context,f"Using mesh export preset {preset_name}") print_message(context, f"Using {sbp.export_format} preset values - {preset_name}") kwargs["filepath"] = str(filepath).encode('utf-8').decode('utf-8') kwargs[use_selection_name] = True operator(**kwargs) else: print_message(context,f"No mesh export preset. Using default export settings") myargs["filepath"] = str(filepath).encode('utf-8').decode('utf-8') operator(**myargs) #Not merged bake, sub-folder, combined mesh if not self.merged_bake and self.export_folder_per_object and self.mesh_mode != 'individual': filepath = base_save_folder / (mesh_export_name + extension) #Make sure folder definitely exists create_dir_if_needed(base_save_folder) select_only_these(context, objs) do_export(filepath) #Folder per object, but not merged bake OR folder per object and merged bake but want individual meshes elif self.export_folder_per_object and ((not self.merged_bake) or (self.merged_bake and self.mesh_mode=="individual")): for obj in objs: if "SB_copy_and_apply_from" in obj: original_name = obj["SB_copy_and_apply_from"] else: original_name = obj.name filepath = base_save_folder / original_name / (original_name + extension) #Make sure folder definitely exists create_dir_if_needed(base_save_folder / original_name) select_only_this(context, obj) do_export(filepath) #Folder per object and merged bake. Combined mesh elif self.export_folder_per_object and self.merged_bake: filepath = base_save_folder / self.merged_bake_name / (mesh_export_name + extension) create_dir_if_needed(base_save_folder / self.merged_bake_name) select_only_these(context, objs) do_export(filepath) #Not folder per object, but we still want individual mesh files elif self.mesh_mode=="individual": for obj in objs: if "SB_copy_and_apply_from" in obj: original_name = obj["SB_copy_and_apply_from"] else: original_name = obj.name filepath = base_save_folder / (original_name + extension) #Make sure folder definitely exists create_dir_if_needed(base_save_folder) select_only_this(context, obj) do_export(filepath) #Not folder per object (either merged bake or not), but not apply to original objects else: filepath = base_save_folder / (mesh_export_name + extension) create_dir_if_needed(base_save_folder) select_only_these(context, objs) do_export(filepath) #If we didn't actually want copy and apply, remove the objects (unless in background #AND UNLESS we are applying bakes to original objects if not self.copy_and_apply and not self.in_background and not sbp.apply_bakes_to_original: [bpy.data.objects.remove(o) for o in objs] #If we merged the objects for export, remove the duplicate file if sbp.merge_export_obj and sbp.export_mesh_individual_or_combined == "combined" and sbp.merged_bake: if o:=bpy.data.objects.get(merged_mesh_obj_name): bpy.data.objects.remove(o) return {'FINISHED'} classes = ([ SimpleBake_OT_Save_Images_Externally, SimpleBake_OT_Save_Objects_Externally ]) def register(): global classes for cls in classes: register_class(cls) def unregister(): global classes for cls in classes: unregister_class(cls)