import bpy from bpy.types import Operator, Macro from bpy.props import StringProperty, BoolProperty from bpy.utils import register_class, unregister_class import sys from pathlib import Path import base64 import mathutils import tempfile import os import numpy as np suppress_for_preset_load = False _disable_impossible_running = False def blender_refresh(): return True # print_message(context, "Blender refresh") # bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) # for area in context.screen.areas: # area.tag_redraw() # context.view_layer.update() # context.view_layer.depsgraph.update() #Takes a path object def can_write_to_location(dir_path): #First check if the folder already exists or not if not os.path.exists(str(dir_path)): #Find the first parent that does exist, see if we can write there last = "" while not os.path.exists(str(dir_path)): dir_path = dir_path.parent if str(dir_path) == last: break last = str(dir_path) test_file_path = dir_path / "test_file.tmp" try: # Attempt to create a temporary file in the directory with open(str(test_file_path), 'w') as temp_file: temp_file.write("Test") # If successful, remove the test file os.remove(str(test_file_path)) return True except PermissionError: # Permission denied return False except Exception as e: # Other exceptions could be handled differently if needed print(f"An error occurred: {e}") return False def disable_impossible(context): global suppress_for_preset_load, _disable_impossible_running if _disable_impossible_running: return _disable_impossible_running = True try: sbp = context.scene.SimpleBake_Props suppress_for_preset_load = True #Start with a refresh of the objects list bpy.ops.simplebake.refresh_bake_object_list() #And AOVs bpy.ops.simplebake.refresh_aov_list() #And UVs bpy.ops.simplebake.sync_uv_list() if bpy.app.version >= (4,0,0): sbp.selected_transrough = False sbp.selected_ssscol = False if not sbp.save_bakes_external: sbp.rundenoise = False sbp.rough_glossy_switch = SBConstants.PBR_ROUGHNESS sbp.ccrough_glossy_switch = SBConstants.PBR_CLEARCOAT_ROUGH sbp.normal_format_switch = SBConstants.NORMAL_OPENGL sbp.multiply_diffuse_ao = "purediffuse" sbp.do_aa = False sbp.save_obj_external = False for cpt in sbp.cp_list: if "png_compression" not in cpt: cpt.png_compression = 15 #if sbp.fbx_preset_name == "": #sbp.fbx_preset_name = "None" if sbp.multiply_diffuse_ao == "diffusexao" and sbp.global_mode == SBConstants.PBR: sbp.selected_ao = True if sbp.s2a_opmode == "decals": sbp.multiply_diffuse_ao = "purediffuse" if not sbp.keep_internal_after_export: sbp.copy_and_apply = False if sbp.tex_per_mat and (sbp.selected_s2a or sbp.cycles_s2a): sbp.tex_per_mat = False if sbp.tex_per_mat and sbp.expand_mat_uvs: sbp.new_uv_option = True if sbp.tex_per_mat and not sbp.expand_mat_uvs: sbp.new_uv_option = False if not sbp.merged_bake or not (sbp.export_mesh_individual_or_combined == "combined"): sbp.merge_export_obj = False if not sbp.merged_bake: sbp.join_objs_to_proxy = False finally: suppress_for_preset_load = False _disable_impossible_running = False def get_bake_objects(context): sbp = context.scene.SimpleBake_Props #Work out our object selection objects = [] if (sbp.selected_s2a or sbp.cycles_s2a) and sbp.s2a_opmode=="automatch": from .bake_operators.auto_match_operators import SimpleBake_OT_AutoMatch_Operation as amo #objects = [context.scene.objects[i] for i in list(amo.hl_matches.keys())] objects = list(amo.hl_matches.keys()) elif sbp.selected_s2a: #Standard S2A bake or Decals objects = [sbp.targetobj.name] if sbp.targetobj != None else [] elif sbp.cycles_s2a: #Standard S2A bake objects = [sbp.targetobj_cycles.name] if sbp.targetobj_cycles != None else [] else: objects = [i.obj_point.name for i in sbp.objects_list] return objects #active_tiles is sorted list of UDIM numbers (1001, 1002 etc.) #total_tiles is highest tile index (e.g. 11 if the highest numbered active tile is 1011) def get_udim_tiles(context, object_name, threshold=0.001): import numpy as np obj = context.scene.objects.get(object_name) if not obj or not hasattr(obj.data, 'uv_layers') or not obj.data.uv_layers: return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": 0} uv_layer = obj.data.uv_layers.active if uv_layer is None: return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": 0} n = len(uv_layer.data) if n == 0: return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": 0} buf = np.empty(n * 2, dtype=np.float64) uv_layer.data.foreach_get("uv", buf) uvs = buf.reshape(-1, 2) u = uvs[:, 0].copy() v = uvs[:, 1].copy() # Snap UV coordinates that sit very close to the upper edge of a tile back # into that tile, to avoid a vertex at e.g. v=0.9999 being counted as tile 1. frac_u = u % 1.0 frac_v = v % 1.0 u[frac_u > 1.0 - threshold] -= threshold v[frac_v > 1.0 - threshold] -= threshold # Subtract a tiny epsilon before flooring so that vertices sitting exactly # on an integer boundary (e.g. v=1.0, where 1.0 % 1.0 == 0.0) snap to the # tile below rather than the tile above. tu = np.floor(u - 1e-9).astype(np.int32) tv = np.floor(v - 1e-9).astype(np.int32) mask = (tu >= 0) & (tu <= 9) & (tv >= 0) & (tv <= 9) tile_nums = 1001 + tu[mask] + 10 * tv[mask] import math u_extent = math.ceil(float(np.max(u))) - math.floor(float(np.min(u))) v_extent = math.ceil(float(np.max(v))) - math.floor(float(np.min(v))) bbox_tiles = max(1, u_extent) * max(1, v_extent) if tile_nums.size == 0: return {"active_tiles": [], "total_tiles": 0, "bbox_tiles": bbox_tiles} udim_tiles = set(np.unique(tile_nums).tolist()) return { "active_tiles": sorted(udim_tiles), "total_tiles": max(udim_tiles) - 1000, "bbox_tiles": bbox_tiles } # def get_max_udim_tiles_from_bake_objects(context): # sbp = context.scene.SimpleBake_Props # objs = [i.name for i in sbp.objects_list] # # max = 0 # # for obj_name in objs: # try: # d = get_udim_tiles(context, obj_name) # if len(d["active_tiles"]) > max: # max = len(d["active_tiles"]) # except: # pass # # return max # --- UDIM tile cache ----------------------------------------------------------- # Keyed by object name. Dirty set contains names needing recompute. _udim_tile_cache: dict = {} _udim_dirty_objects: set = set() def invalidate_udim_cache(object_names=None): """Mark objects as needing recompute. Pass None to invalidate everything.""" global _udim_dirty_objects, _udim_tile_cache if object_names is None: _udim_tile_cache.clear() _udim_dirty_objects.clear() else: for name in object_names: _udim_dirty_objects.add(name) @bpy.app.handlers.persistent def _sb_udim_depsgraph_handler(scene, depsgraph): for update in depsgraph.updates: if not update.is_updated_geometry: continue id_data = update.id if isinstance(id_data, bpy.types.Object) and id_data.type == 'MESH': # Topology change on the object itself _udim_dirty_objects.add(id_data.name) elif isinstance(id_data, bpy.types.Mesh): # UV edits come through as mesh data-block updates — # find all objects that use this mesh and dirty them for obj in scene.objects: if obj.type == 'MESH' and obj.data == id_data: _udim_dirty_objects.add(obj.name) def get_cached_udim_tiles(context, object_name, threshold=0.001): global _udim_tile_cache, _udim_dirty_objects if object_name in _udim_dirty_objects or object_name not in _udim_tile_cache: #Cache result from the "real" method _udim_tile_cache[object_name] = get_udim_tiles(context, object_name, threshold) #Now clean _udim_dirty_objects.discard(object_name) return _udim_tile_cache[object_name] # ------------------------------------------------------------------------------ def find_closest_obj(context, object_name, max_distance=0.5): ref_obj = context.scene.objects.get(object_name) if not ref_obj: raise ValueError(f"No object found with the name '{object_name}'") closest_mesh = None closest_distance = float('inf') for obj in context.scene.objects: if obj.type == 'MESH' and obj != ref_obj and "SB_replaced_orig_name" not in obj: distance = (ref_obj.location - obj.location).length if distance < closest_distance: closest_distance = distance closest_mesh = obj return closest_mesh if closest_distance <= max_distance else None class SBConstants: #Constants PBR = "PBR" PBRS2A = "PBR StoA" PBRS2A_AUTOMATCH = "PBR StoA AutoMatch" PBRS2A_DECALS = "PBR StoA Decals" CYCLESBAKE = "CyclesBake" CYCLESBAKE_S2A = "CyclesBake S2A" CYCLESBAKES2A_AUTOMATCH = "CyclesBake S2A Auto" SPECIALS = "Specials" SPECIALS_PBR_TARGET_ONLY = "specials_pbr_targetonly" SPECIALS_CYCLES_TARGET_ONLY = "specials_cycles_targetonly" PBRAOVS = "AOV" #PBR Names PBR_DIFFUSE = "Diffuse" PBR_METAL = "Metalness" PBR_SSS = "SSS" PBR_SSS_SCALE = "SSS Scale" PBR_SSSCOL = "SSS Colour" PBR_ROUGHNESS = "Roughness" PBR_GLOSSY = "Glossiness" PBR_NORMAL = "Normal" PBR_TRANSMISSION = "Transmission" PBR_TRANSMISSION_ROUGH = "Transmission Roughness" PBR_CLEARCOAT = "Clearcoat" PBR_CLEARCOAT_ROUGH = "Clearcoat Roughness" PBR_CLEARCOAT_GLOSS = "Clearcoat Glossiness" PBR_EMISSION = "Emission" PBR_EMISSION_STRENGTH = "Emission Strength" PBR_SPECULAR = "Specular" PBR_ALPHA = "Alpha" PBR_BUMP = "Bump" PBR_DISPLACEMENT = "Displacement" ALL_PBR_MODES = ([PBR_DIFFUSE, PBR_METAL, PBR_SSS,PBR_SSS_SCALE, PBR_SSSCOL, PBR_ROUGHNESS, PBR_GLOSSY, PBR_NORMAL, PBR_TRANSMISSION, PBR_TRANSMISSION_ROUGH, PBR_CLEARCOAT, PBR_CLEARCOAT_ROUGH, PBR_CLEARCOAT_GLOSS, PBR_EMISSION, PBR_EMISSION_STRENGTH, PBR_SPECULAR, PBR_ALPHA, PBR_BUMP, PBR_DISPLACEMENT]) #Specials names THICKNESS = "Thickness" AO = "Ambient Occlusion" CURVATURE = "Curvature" COLOURID = "Colour ID" VERTEXCOL = "Vertex Colour" LIGHTMAP = "Lightmap" ALL_SPECIALS = ([THICKNESS, AO, CURVATURE,COLOURID, VERTEXCOL, LIGHTMAP]) COMP_DENOISE_SPECIALS = ([THICKNESS, AO, LIGHTMAP]) COMP_NODENOISE_SPECIALS = ([CURVATURE, VERTEXCOL, COLOURID]) #UVs NEWUV_SMART_INDIVIDUAL = "SmartUVProject_Individual" NEWUV_SMART_ATLAS = "SmartUVProject_Atlas" NEWUV_COMBINE_EXISTING = "CombineExisting" #Normals NORMAL_OPENGL = "OpenGL" NORMAL_DIRECTX = "DirectX" @classmethod def get_socket_names(cls): v = bpy.app.version if v >= (4,0,0): return { cls.PBR_DIFFUSE: "Base Color", cls.PBR_METAL: "Metallic", cls.PBR_SSS: "Subsurface Weight", cls.PBR_SSS_SCALE: "Subsurface Scale", cls.PBR_SSSCOL: None, cls.PBR_ROUGHNESS: "Roughness", cls.PBR_GLOSSY: "Roughness", cls.PBR_NORMAL: "Normal", cls.PBR_TRANSMISSION: "Transmission Weight", cls.PBR_TRANSMISSION_ROUGH: None, cls.PBR_CLEARCOAT: "Coat Weight", cls.PBR_CLEARCOAT_ROUGH: "Coat Roughness", cls.PBR_CLEARCOAT_GLOSS: "Coat Roughness", cls.PBR_EMISSION: "Emission Color", cls.PBR_EMISSION_STRENGTH: "Emission Strength", cls.PBR_SPECULAR: "Specular IOR Level", cls.PBR_ALPHA: "Alpha" } else: #Principled socket names return { cls.PBR_DIFFUSE: "Base Color", cls.PBR_METAL: "Metallic", cls.PBR_SSS: "Subsurface", cls.PBR_SSS_SCALE: "Subsurface Scale", cls.PBR_SSSCOL: "Subsurface Color", cls.PBR_ROUGHNESS: "Roughness", cls.PBR_GLOSSY: "Roughness", cls.PBR_NORMAL: "Normal", cls.PBR_TRANSMISSION: "Transmission", cls.PBR_TRANSMISSION_ROUGH: "Transmission Roughness", cls.PBR_CLEARCOAT: "Clearcoat", cls.PBR_CLEARCOAT_ROUGH: "Clearcoat Roughness", cls.PBR_CLEARCOAT_GLOSS: "Clearcoat Glossiness", cls.PBR_EMISSION: "Emission", cls.PBR_EMISSION_STRENGTH: "Emission Strength", cls.PBR_SPECULAR: "Specular", cls.PBR_ALPHA: "Alpha" } def get_texture_channels(context, tex): sbp = context.scene.SimpleBake_Props if sbp.global_mode in [SBConstants.CYCLESBAKE, SBConstants.CYCLESBAKE_S2A]: return "RGB" elif tex in [SBConstants.PBR_DIFFUSE, SBConstants.PBR_SSSCOL, SBConstants.PBR_EMISSION, SBConstants.PBR_NORMAL, SBConstants.COLOURID, SBConstants.VERTEXCOL, SBConstants.LIGHTMAP]: return "RGB" elif SBConstants.PBRAOVS in tex: num = int(tex.replace(f"{SBConstants.PBRAOVS}_", "")) aov = [i for i in sbp.aov_items if i.aov_number == num] assert len(aov)>0, "Unable to find matching AOV" aov = aov[0] if aov.aov_type == "C": return "RGB" else: return "RGB" if sbp.bw_to_rgb else "BW" else: return "RGB" if sbp.bw_to_rgb else "BW" def object_list_to_names_list(context): sbp = context.scene.SimpleBake_Props names = sbp.objects_list.items() names = [n[0] for n in names] return names def get_base_folder_patho(context): sbp = context.scene.SimpleBake_Props if "--background" in sys.argv or sbp.fake_background: base_folder = Path(sbp.base_folder_override) else: base_folder = Path(bpy.data.filepath).parent return base_folder def force_to_object_mode(context): sbp = context.scene.SimpleBake_Props def object_in_view_layer(obj): return obj is not None and any(view_obj == obj for view_obj in context.view_layer.objects) if not object_in_view_layer(context.active_object): for item in sbp.objects_list: obj = item.obj_point if object_in_view_layer(obj): context.view_layer.objects.active = obj break if context.active_object is None: for obj in context.view_layer.objects: if obj.type == "MESH" and obj.visible_get(): context.view_layer.objects.active = obj break if context.active_object is None: show_message_box(context, ["SimpleBake could not switch to Object Mode because Blender has no usable active mesh object in the current view layer."], "Errors occured", icon='ERROR') return False if context.mode != "OBJECT": bpy.ops.object.mode_set(mode="OBJECT", toggle=False) return context.mode == "OBJECT" # def check_for_safe_context_class(context): # sbp = context.scene.SimpleBake_Props # t = [] # #t.append(base64.b64decode("bGV3aXM=").decode("utf-8")) # if sbp.safe_context_check in t: # print_message(context, "Warning - Safe context class check failed. This is not fatal. Continuing") # return False # else: # return True # # return True # def select_selected_to_active(context, selected_objs, active_obj): # bpy.ops.object.select_all(action="DESELECT") # # for obj in selected_objs: # obj.select_set(state=True) # active_obj.select_set(state=True) # context.view_layer.objects.active = active_obj def select_only_these(context, objs): #When running an image sequence, this sometimes gets called with an empty list and I don't know why if len(objs) < 1: return False #bpy.ops.object.select_all(action="DESELECT") [o.select_set(state=False) for o in context.scene.objects] for obj in objs: obj.select_set(state=True) context.view_layer.objects.active = objs[0] #arbitary def select_only_this(context, obj): #bpy.ops.object.select_all(action="DESELECT") [o.select_set(state=False) for o in context.scene.objects] obj.select_set(state=True) context.view_layer.objects.active = obj def pbr_selections_to_list(context): sbp = context.scene.SimpleBake_Props selectedbakes = [] selectedbakes.append(SBConstants.PBR_DIFFUSE) if sbp.selected_col else False selectedbakes.append(SBConstants.PBR_METAL) if sbp.selected_metal else False selectedbakes.append(SBConstants.PBR_ROUGHNESS) if sbp.selected_rough and sbp.rough_glossy_switch==SBConstants.PBR_ROUGHNESS else False selectedbakes.append(SBConstants.PBR_GLOSSY) if sbp.selected_rough and sbp.rough_glossy_switch==SBConstants.PBR_GLOSSY else False selectedbakes.append(SBConstants.PBR_NORMAL) if sbp.selected_normal else False selectedbakes.append(SBConstants.PBR_TRANSMISSION) if sbp.selected_trans else False selectedbakes.append(SBConstants.PBR_TRANSMISSION_ROUGH) if sbp.selected_transrough else False selectedbakes.append(SBConstants.PBR_CLEARCOAT) if sbp.selected_clearcoat else False selectedbakes.append(SBConstants.PBR_CLEARCOAT_ROUGH) if sbp.selected_clearcoat_rough and sbp.ccrough_glossy_switch==SBConstants.PBR_CLEARCOAT_ROUGH else False selectedbakes.append(SBConstants.PBR_CLEARCOAT_GLOSS) if sbp.selected_clearcoat_rough and sbp.ccrough_glossy_switch==SBConstants.PBR_CLEARCOAT_GLOSS else False selectedbakes.append(SBConstants.PBR_EMISSION) if sbp.selected_emission else False selectedbakes.append(SBConstants.PBR_EMISSION_STRENGTH) if sbp.selected_emission_strength else False selectedbakes.append(SBConstants.PBR_SPECULAR) if sbp.selected_specular else False selectedbakes.append(SBConstants.PBR_ALPHA) if sbp.selected_alpha else False selectedbakes.append(SBConstants.PBR_SSS) if sbp.selected_sss else False selectedbakes.append(SBConstants.PBR_SSS_SCALE) if sbp.selected_sss_scale else False selectedbakes.append(SBConstants.PBR_SSSCOL) if sbp.selected_ssscol else False selectedbakes.append(SBConstants.PBR_BUMP) if sbp.selected_bump else False selectedbakes.append(SBConstants.PBR_DISPLACEMENT) if sbp.selected_displacement else False for aov in sbp.aov_items: if aov.enabled: selectedbakes.append(SBConstants.PBRAOVS + f"_{aov.aov_number}") return selectedbakes def specials_selection_to_list(context): sbp = context.scene.SimpleBake_Props selected_specials = [] selected_specials.append(SBConstants.THICKNESS) if sbp.selected_thickness else False selected_specials.append(SBConstants.COLOURID) if sbp.selected_col_mats else False selected_specials.append(SBConstants.VERTEXCOL) if sbp.selected_col_vertex else False selected_specials.append(SBConstants.CURVATURE) if sbp.selected_curvature else False selected_specials.append(SBConstants.LIGHTMAP) if sbp.selected_lightmap else False selected_specials.append(SBConstants.AO) if sbp.selected_ao else False return selected_specials def _find_colorspace_fallback(cs_names, fallback_names): if not cs_names: return (0, "") normalised_names = {name.casefold(): i for i, name in enumerate(cs_names)} for fallback_name in fallback_names: i = normalised_names.get(fallback_name.casefold()) if i is not None: return (i, cs_names[i]) for fallback_name in fallback_names: fallback_name = fallback_name.casefold() for i, cs_name in enumerate(cs_names): if fallback_name in cs_name.casefold(): return (i, cs_name) return (0, cs_names[0]) def blender_default_colorspace(float_buffer: bool, col: bool): col_index = 0 non_col_index = 0 cs_names = [cs.identifier for cs in bpy.types.Image.bl_rna.properties['colorspace_settings'].fixed_type.bl_rna.properties['name'].enum_items] if bpy.app.version >= (5,0,0): col_index = 23 non_col_index = 18 elif bpy.app.version >= (4,2,0): col_index = 20 non_col_index = 17 elif bpy.app.version >= (4,0,0): col_index = 19 non_col_index = 16 else: assert False, "Blender version too old" if float_buffer or not col: if non_col_index < len(cs_names): return (non_col_index, cs_names[non_col_index]) return _find_colorspace_fallback(cs_names, ["Non-Color", "Non-Colour", "Raw", "Data"]) else: if col_index < len(cs_names): return (col_index, cs_names[col_index]) return _find_colorspace_fallback(cs_names, ["sRGB", "AgX Base sRGB", "Linear Rec.709"]) def get_cs_list(self, context): cs_items = [(cs.identifier,cs.identifier,"") for cs in bpy.types.Image.bl_rna.properties['colorspace_settings'].fixed_type.bl_rna.properties['name'].enum_items] return cs_items def clean_file_name(filename): keepcharacters = (' ','.','_','~',"-", "[", "]") return "".join(c for c in filename if c.isalnum() or c in keepcharacters)#.rstrip() def transfer_tags(old_img, new_img): tags= ([ "SB_bake_object", "SB_global_mode", "SB_this_bake", "SB_merged_bake", "SB_merged_bake_name", "SB_bake_operation_id", "SB_float_buffer", "SB_scaled", "SB_exported_list" ]) for tag in tags: if tag in old_img: new_img[tag] = old_img[tag] return True class SimpleBake_OT_Halt(Operator): bl_idname = "simplebake.halt" bl_label = "" def execute(self, context): raise Exception return {'FINISHED'} def show_message_box(context, messageitems_list, title, icon = 'INFO'): message="" for li in messageitems_list: message+=f"{li}|" bpy.ops.simplebake.show_message_box('INVOKE_DEFAULT', message=message, centre=True, icon=icon) # def draw(self, context): # for m in messageitems_list: # self.layout.label(text=m) # context.window_manager.popup_menu(draw, title = title, icon = icon) return True class SimpleBake_OT_Show_Message_Box(Operator): bl_idname = "simplebake.show_message_box" bl_label = "" message: StringProperty() icon: StringProperty(default="INFO") centre: BoolProperty(default=False) def execute(self, context): return {'FINISHED'} def invoke(self, context, event): self.restored = False if self.centre: self.orig_x = event.mouse_x self.orig_y = event.mouse_y w = int(context.window.width/2) h = int(context.window.height/2) h = h + (20*len(self.message.split("|"))) context.window.cursor_warp(w, h) longest = 0 for li in self.message.split("|"): longest = len(li) if (len(li)>longest) else longest width = longest*6 if (longest*6)>=400 else 400 return context.window_manager.invoke_props_dialog(self, width = width) def draw(self, context): self.layout.label(text="SIMPLEBAKE", icon=self.icon) message_list = self.message.split("|") for li in message_list: row=self.layout.row() row.scale_y = 0.8 row.alignment = 'LEFT' row.label(text=li) if not self.restored and self.centre: context.window.cursor_warp(self.orig_x, self.orig_y) self.restored = True def find_3d_viewport(context=None, screen=None): if context == None: context = bpy.context if screen == None: screen = bpy.context.window.screen found = [] # Iterate through the areas in the current screen for area in screen.areas: # Check if the area is a 3D viewport if area.type == 'VIEW_3D': # Optionally, you can access the space data of the area for space in area.spaces: if space.type == 'VIEW_3D': found.append(space) return found def is_blend_saved(): path = bpy.data.filepath if(path=="/" or path==""): return False else: return True def is_all_black_or_transparent(img: bpy.types.Image, tol=1e-6): if not img.has_data: img.update() # Blender exposes pixels as RGBA floats in [0..1]; length = w*h*4 px = np.asarray(img.pixels[:], dtype=np.float32).reshape(-1, 4) rgb = px[:, :3] a = px[:, 3] all_black = np.all(np.abs(rgb) <= tol) # R=G=B≈0 everywhere all_transparent = np.all(a <= tol) # A≈0 everywhere return all_black, all_transparent conflicting_addons = []#"ZenUV"] classes = ([ SimpleBake_OT_Show_Message_Box, #SimpleBake_OT_Print_Message, SimpleBake_OT_Halt ]) def register(): global classes for cls in classes: register_class(cls) if _sb_udim_depsgraph_handler not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(_sb_udim_depsgraph_handler) def unregister(): global classes for cls in classes: unregister_class(cls) if _sb_udim_depsgraph_handler in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(_sb_udim_depsgraph_handler)