import bpy import os import time from . import utilities_ui from . import utilities_uv from . import settings from . import utilities_bake as ub # Notes: https://docs.blender.org/manual/en/dev/render/blender_render/bake.html modes={ #'displacement': ub.BakeMode('', type='DISPLACEMENT', use_project=True, color=(0, 0, 0, 1), engine='CYCLES'), 'normal_tangent': ub.BakeMode('', type='NORMAL', color=(0.5, 0.5, 1, 1), use_project=True), 'normal_object': ub.BakeMode('', type='NORMAL', color=(0.5, 0.5, 1, 1), normal_space='OBJECT'), 'bevel_mask': ub.BakeMode('bake_bevel_mask', type='EMIT', color=(0, 0, 0, 1), params=["bake_bevel_samples","bake_bevel_size"]), 'normal_tangent_bevel': ub.BakeMode('bake_bevel_normal', type='NORMAL', color=(0.5, 0.5, 1, 1), params=["bake_bevel_samples","bake_bevel_size"]), 'normal_object_bevel': ub.BakeMode('bake_bevel_normal', type='NORMAL', color=(0.5, 0.5, 1, 1), normal_space='OBJECT', params=["bake_bevel_samples","bake_bevel_size"]), 'thickness': ub.BakeMode('bake_thickness', type='EMIT', color=(0, 0, 0, 1), params=["bake_samples","bake_thickness_distance","bake_thickness_contrast","bake_thickness_local"]), 'cavity': ub.BakeMode('bake_cavity', type='EMIT', setVColor=ub.setup_vertex_color_dirty), 'paint_base': ub.BakeMode('bake_paint_base', type='EMIT'), 'dust': ub.BakeMode('bake_dust', type='EMIT', setVColor=ub.setup_vertex_color_dirty), 'ao': ub.BakeMode('', type='AO', params=["bake_samples"], engine='CYCLES'), 'position': ub.BakeMode('bake_position', type='EMIT'), 'curvature': ub.BakeMode('', type='NORMAL', use_project=True, params=["bake_curvature_size"], composite="curvature"), 'wireframe': ub.BakeMode('bake_wireframe', type='EMIT', color=(0, 0, 0, 1), params=["bake_wireframe_size"]), 'id_element': ub.BakeMode('bake_vertex_color', type='EMIT', setVColor=ub.setup_vertex_color_id_element), 'id_material': ub.BakeMode('bake_vertex_color', type='EMIT', setVColor=ub.setup_vertex_color_id_material), 'selection': ub.BakeMode('bake_vertex_color', type='EMIT', color=(0, 0, 0, 1), setVColor=ub.setup_vertex_color_selection), 'diffuse': ub.BakeMode('', type='DIFFUSE'), 'base_color': ub.BakeMode('', type='EMIT', relink = {'needed':True, 'b':ub.chs['ech'], 'n':0}), 'sss_strength': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['ssch']}), 'sss_color': ub.BakeMode('', type='EMIT', relink = {'needed':True, 'b':ub.chs['ech'], 'n':ub.chs['scch']}), 'metallic': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['mch']}), 'specular': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['sch']}), 'specular_tint': ub.BakeMode('', type='EMIT', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['ech'], 'n':ub.chs['stch']}), 'roughness': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1)), 'glossiness': ub.BakeMode('', type='ROUGHNESS', color=(1, 1, 1, 1), invert=True), 'anisotropic': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['ach']}), 'anisotropic_rotation': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['arch']}), 'sheen': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['shch']}), 'sheen_tint': ub.BakeMode('', type='EMIT', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['ech'], 'n':ub.chs['shtch']}), 'clearcoat': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['cch']}), 'clearcoat_roughness': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['crch']}), 'transmission': ub.BakeMode('', type='TRANSMISSION'), 'transmission_roughness': ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['trch']}), 'emission': ub.BakeMode('', type='EMIT', color=(0, 0, 0, 1)), 'environment': ub.BakeMode('', type='ENVIRONMENT'), 'uv': ub.BakeMode('', type='UV'), 'shadow': ub.BakeMode('', type='SHADOW', params=["bake_samples"]), 'combined': ub.BakeMode('', type='COMBINED', params=["bake_samples"]) } if settings.bversion >= 2.91: modes['emission_strength']= ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['esch']}) modes['alpha']= ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['alch']}) else: modes['alpha']= ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':ub.chs['rch'], 'n':ub.chs['esch']}) class op(bpy.types.Operator): bl_idname = "uv.textools_bake" bl_label = "Bake" bl_description = "Bake selected objects" @classmethod def poll(cls, context): if len(settings.sets) == 0: settings.bake_error = "" return False bake_mode = utilities_ui.get_bake_mode() if bake_mode not in modes: settings.bake_error = "" return False if bake_mode in {'ao','normal_tangent','normal_object','curvature','environment','uv','shadow'}: settings.bake_error = "" return True if bake_mode == 'combined': if (not bpy.context.scene.render.bake.use_pass_direct) and (not bpy.context.scene.render.bake.use_pass_indirect) and (not bpy.context.scene.render.bake.use_pass_emit): settings.bake_error = "Lighting or Emit needed" return False settings.bake_error = "" return True if modes[bake_mode].setVColor or not modes[bake_mode].material: def is_bakeable(obj): if len(obj.data.materials) <= 0: # There are no material slots settings.bake_error = "Materials needed" return False elif not any(obj.data.materials): # All material slots are empty settings.bake_error = "Materials needed" return False else: for slot in obj.material_slots: if slot.material is not None: if slot.material.use_nodes == False: settings.bake_error = "Nodal materials needed" return False bsdf_node = None for n in slot.material.node_tree.nodes: if n.bl_idname == "ShaderNodeBsdfPrincipled": bsdf_node = n if not bsdf_node: bool_alpha_ignore = bpy.context.preferences.addons[__package__].preferences.bool_alpha_ignore bool_clean_transmission = bpy.context.preferences.addons[__package__].preferences.bool_clean_transmission builtin_modes_material = {'diffuse','emission','roughness','glossiness','transmission'} if modes[bake_mode].relink['needed'] or (bool_clean_transmission and bake_mode == 'transmission') or (bool_alpha_ignore and bake_mode not in builtin_modes_material): settings.bake_error = "BSDF nodes needed" return False # else: # settings.bake_error = "Materials needed" # return False settings.bake_error = "" return True def is_vc_ready(obj): if len(obj.data.vertex_colors) > 7: settings.bake_error = "An empty VC layer needed" return False settings.bake_error = "" return True for bset in settings.sets: if (len(bset.objects_high) + len(bset.objects_float)) == 0: if not modes[bake_mode].material: for obj in bset.objects_low: if not is_bakeable(obj): return False if modes[bake_mode].setVColor: for obj in bset.objects_low: if not is_vc_ready(obj): return False else: if not modes[bake_mode].material: for obj in (bset.objects_high + bset.objects_float): if not is_bakeable(obj): return False if modes[bake_mode].setVColor: for obj in (bset.objects_high + bset.objects_float): if not is_vc_ready(obj): return False settings.bake_error = "" return True def execute(self, context): startTime = time.monotonic() preferences = bpy.context.preferences.addons[__package__].preferences circular_report = [False, ] color_report = [False, ] if preferences.bool_clean_transmission: modes['transmission']= ub.BakeMode('', type='ROUGHNESS', color=(0, 0, 0, 1), relink = {'needed':True, 'b':7, 'n':15}) else: modes['transmission']= ub.BakeMode('', type='TRANSMISSION') bake_mode = utilities_ui.get_bake_mode() if bake_mode not in modes: self.report({'ERROR_INVALID_INPUT'}, "Unknown mode '{}' only available: '{}'".format(bake_mode, ", ".join(modes.keys() )) ) return {'CANCELLED'} # Store Selection selected_objects = [obj for obj in bpy.context.selected_objects] active_object = bpy.context.view_layer.objects.active pre_selection_mode = None if active_object: pre_selection_mode = bpy.context.active_object.mode ub.store_bake_settings() if preferences.bake_device != 'DEFAULT': bpy.context.scene.cycles.device = preferences.bake_device bpy.context.scene.render.engine = modes[bake_mode].engine #Switch render engine # Avoid weird rendering problems when Progressive Refine is activated from Blender 2.90 if settings.bversion < 3: bpy.context.scene.cycles.use_progressive_refine = False # Make it sure that an Image, and not a Vertex Colors layer, is the target of the bake if settings.bversion >= 2.92: bpy.context.scene.render.bake.target = 'IMAGE_TEXTURES' # Disable denoising until it is properly implemented for baking if settings.bversion >= 3: bpy.context.scene.cycles.use_denoising = False # Render sets bake( self = self, mode = bake_mode, size = bpy.context.scene.texToolsSettings.size, bake_force = bpy.context.scene.texToolsSettings.bake_force, sampling_scale = int(bpy.context.scene.texToolsSettings.bake_sampling), samples = bpy.context.scene.texToolsSettings.bake_samples, cage_extrusion = bpy.context.scene.texToolsSettings.bake_cage_extrusion, ray_distance = bpy.context.scene.texToolsSettings.bake_ray_distance, circular_report = circular_report, color_report = color_report, selected = selected_objects, active = active_object, pre_selection_mode = pre_selection_mode ) elapsed = round(time.monotonic()-startTime, 2) if circular_report[0]: if color_report[0]: self.report({'WARNING'}, "Possible Circular Dependency: a previously baked image may have affected the new bake; " + color_report[0] + "Baking finished in " + str(elapsed) + "s.") else: self.report({'WARNING'}, "Possible Circular Dependency: a previously baked image may have affected the new bake. Baking finished in " + str(elapsed) + "s.") else: if color_report[0]: self.report({'WARNING'}, color_report[0] + ". Baking finished in " + str(elapsed) + "s.") else: self.report({'INFO'}, "Baking finished in " + str(elapsed) + "s.") return {'FINISHED'} def bake(self, mode, size, bake_force, sampling_scale, samples, cage_extrusion, ray_distance, circular_report, color_report, selected, active, pre_selection_mode): print("Bake '{}'".format(mode)) # Get the baking sets / pairs sets = settings.sets for bset in sets: # Requires 1+ low poly objects if len(bset.objects_low) == 0: self.report({'ERROR_INVALID_INPUT'}, "No low poly object as part of the '{}' set".format(bset.name) ) return {'CANCELLED'} # Check for UV maps for obj in bset.objects_low: if (not obj.data.uv_layers) or len(obj.data.uv_layers) == 0: self.report({'ERROR_INVALID_INPUT'}, "No UV map available for '{}'".format(obj.name)) return {'CANCELLED'} # Check for cage inconsistencies if len(bset.objects_cage) > 0 and (len(bset.objects_low) != len(bset.objects_cage)): self.report({'ERROR_INVALID_INPUT'}, "{}x cage objects do not match {}x low poly objects for '{}'".format(len(bset.objects_cage), len(bset.objects_low), obj.name)) return {'CANCELLED'} # Disable edit mode if bpy.context.view_layer.objects.active != None and bpy.context.object.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') bool_emission_strength_ignore = bpy.context.preferences.addons[__package__].preferences.bool_emission_ignore bool_alpha_ignore = bpy.context.preferences.addons[__package__].preferences.bool_alpha_ignore render_width = sampling_scale * size[0] render_height = sampling_scale * size[1] # Get custom materials material_loaded = get_material(mode) # Setup properties of the custom material_loaded if material_loaded is not None: setup_material_loaded(mode, material_loaded) # If baking Material ID, make sure the color for each material is consistent between bakes if mode == 'id_material': # Try to redirect deleted materials which were recovered with undo if len(ub.allMaterials) > 0 : for i, mtl in enumerate(ub.allMaterials): try: mtl.name except: ub.allMaterials[i] = bpy.data.materials.get(ub.allMaterialsNames[i]) else: # Store a persistent ordered list of all originally used materials in the scene ub.allMaterials = [mtl for mtl in bpy.data.materials if (mtl is not None and mtl.users != 0)] ub.allMaterialsNames = [mtl.name for mtl in ub.allMaterials] # If baking Element ID, make sure the color for each element is consistent between bakes if mode == 'id_element': ub.elementsCount = 0 # Create dictionaries to remember original and temporary -copied- materials used in the baked objects previous_materials = {} copied_materials = {} # Container to save existing UDIM tile names of each set tiles = [] for bset in sets: for obj in (bset.objects_low + bset.objects_high + bset.objects_float): if obj not in previous_materials: previous_materials[obj] = [] for i, mtl in enumerate(obj.data.materials): if mtl is None: previous_materials[obj].append(None) else: previous_materials[obj].append(obj.data.materials[i].name) # Substitute original materials for copied ones to avoid restoral after tuning def use_copied_mtls(obj): for i, mtl in enumerate(obj.data.materials): if mtl is not None: if mtl not in copied_materials: mat_copied = mtl.copy() obj.data.materials[i] = mat_copied copied_materials[mtl.name] = mat_copied.name else: obj.data.materials[i] = copied_materials[mtl.name] def use_material_loaded(obj): if len(obj.data.materials) > 0: for i in range(len(obj.data.materials)): obj.data.materials[i] = bpy.data.materials[material_loaded] else: obj.data.materials.append(bpy.data.materials[material_loaded]) for bset in settings.sets: if (len(bset.objects_high) + len(bset.objects_float)) == 0: tiles.append( utilities_uv.get_UDIM_tiles( bset.objects_low ) ) for obj in bset.objects_low: if material_loaded is None: use_copied_mtls(obj) else: use_material_loaded(obj) else: tiles.append( utilities_uv.get_UDIM_tiles( bset.objects_high + bset.objects_float ) ) if material_loaded is None: for obj in (bset.objects_low + bset.objects_high + bset.objects_float): use_copied_mtls(obj) else: for obj in bset.objects_low: use_copied_mtls(obj) for obj in (bset.objects_high+bset.objects_float): use_material_loaded(obj) relinkedMaterials = [] EmissionIgnoredMaterials = [] AlphaIgnoredMaterials = [] bakeReadyMaterials = [] # Store references of materials where the baking image node is ready and an Avoid Circular Dependency action has been taken image = previous_image = imagecopy = None # Store image references globally just in case they have to be used to bake all sets stored_images = [] # [image, previous_image, imagecopy] list of lists try: for s,bset in enumerate(sets): # Get image name name_texture = "{}_{}".format(bset.name, mode) if bake_force == "Single": name_texture = "{}_{}".format(sets[0].name, mode) # In Single mode, bake into the same texture #path = bpy.path.abspath("//{}.tga".format(name_texture)) is_clear = (not bake_force == "Single") or (bake_force == "Single" and s==0) # Setup "image" to bake on and retrieve "previous_image": an image that exists in the blend file with the same name than "image", maybe used in materials involved in the bake if is_clear: bakeReadyMaterials = [] loaded = True if material_loaded is None: loaded = False image, previous_image = setup_image(color_report, mode, name_texture, render_width, render_height, tiles[s], material_load=loaded) # Avoid Circular Dependency method A: Create image copy to use in existing nodes that may be affected if baking directly in a "previous_image" whose source is an external file if image == previous_image: imagecopy = image.copy() else: imagecopy = None image_name = image.name if previous_image is None: previous_image_name = None else: previous_image_name = previous_image.name if imagecopy is None: imagecopy_name = None else: imagecopy_name = imagecopy.name stored_images.append([image_name, previous_image_name, imagecopy_name, name_texture]) def assign_tune_materials(obj, setup_bake_nodes=False): if material_loaded is not None: # If baking ID Materials, update the persistent ordered list of all materials in the scene if mode == 'id_material': for mtlname in previous_materials[obj]: if mtlname is not None: if bpy.data.materials[mtlname] not in ub.allMaterials: ub.allMaterials.append(bpy.data.materials[mtlname]) ub.allMaterialsNames.append(mtlname) if modes[mode].setVColor: ub.assign_vertex_color(obj) if mode == 'id_material': modes[mode].setVColor(obj, previous_materials) else: modes[mode].setVColor(obj) elif modes[mode].relink['needed']: for slot in obj.material_slots: if slot.material: if slot.material not in relinkedMaterials: relink_nodes(mode, slot.material) relinkedMaterials.append(slot.material) if modes[mode].type == 'EMIT' and settings.bversion >= 2.91: if slot.material not in EmissionIgnoredMaterials: channel_ignore(modes['emission_strength'].relink['n'], slot.material) EmissionIgnoredMaterials.append(slot.material) if (bool_alpha_ignore and mode != 'ao' and mode != 'diffuse') or mode == 'alpha': if slot.material not in AlphaIgnoredMaterials: channel_ignore(modes['alpha'].relink['n'], slot.material) AlphaIgnoredMaterials.append(slot.material) if setup_bake_nodes: setup_image_bake_node(obj, bakeReadyMaterials, image_name, previous_image_name, imagecopy_name) elif bool_emission_strength_ignore and settings.bversion >= 2.91 and mode == 'emission': for slot in obj.material_slots: if slot.material: if slot.material.use_nodes: bsdf_node = None for n in slot.material.node_tree.nodes: if n.bl_idname == "ShaderNodeBsdfPrincipled": bsdf_node = n if bsdf_node: if slot.material not in EmissionIgnoredMaterials: channel_ignore(modes['emission_strength'].relink['n'], slot.material) EmissionIgnoredMaterials.append(slot.material) if (bool_alpha_ignore and mode != 'ao' and mode != 'diffuse') or mode == 'alpha': if slot.material not in AlphaIgnoredMaterials: channel_ignore(modes['alpha'].relink['n'], slot.material) AlphaIgnoredMaterials.append(slot.material) if setup_bake_nodes: setup_image_bake_node(obj, bakeReadyMaterials, image_name, previous_image_name, imagecopy_name) else: if (bool_alpha_ignore and mode != 'ao' and mode != 'diffuse') or mode == 'alpha': for slot in obj.material_slots: if slot.material: if slot.material.use_nodes: bsdf_node = None for n in slot.material.node_tree.nodes: if n.bl_idname == "ShaderNodeBsdfPrincipled": bsdf_node = n if bsdf_node: if slot.material not in AlphaIgnoredMaterials: channel_ignore(modes['alpha'].relink['n'], slot.material) AlphaIgnoredMaterials.append(slot.material) if setup_bake_nodes: setup_image_bake_node(obj, bakeReadyMaterials, image_name, previous_image_name, imagecopy_name) # Assign Materials to Objects / tune the existing materials, and distribute temp bake image nodes if (len(bset.objects_high) + len(bset.objects_float)) == 0: # Low poly bake: Assign material to lowpoly or tune the existing material/s for obj in bset.objects_low: if mode in {'ao','normal_tangent','normal_object','curvature','environment','uv','shadow','combined'}: # Clean unused material slots? # if len(obj.data.materials) > 0: # if not any(obj.data.materials): # All material slots are empty # obj.active_material_index = 0 # for i in range(len(obj.material_slots)): # bpy.ops.object.material_slot_remove({'object': obj}) if len(obj.material_slots) == 0 or (not all(obj.data.materials)): if "TT_bake_node" not in bpy.data.materials: bpy.data.materials.new(name="TT_bake_node") if len(obj.material_slots) == 0: obj.data.materials.append(bpy.data.materials["TT_bake_node"]) else: for slot in obj.material_slots: if not slot.material: slot.material = bpy.data.materials["TT_bake_node"] assign_tune_materials(obj, setup_bake_nodes=True) if material_loaded is not None : setup_image_bake_node(bset.objects_low[0], bakeReadyMaterials, image_name, previous_image_name, imagecopy_name) else: # High to low poly: Low poly requires any material to bake into image for obj in bset.objects_low: if len(obj.material_slots) == 0 or (not all(obj.data.materials)): if "TT_bake_node" not in bpy.data.materials: bpy.data.materials.new(name="TT_bake_node") if len(obj.material_slots) == 0: obj.data.materials.append(bpy.data.materials["TT_bake_node"]) else: for slot in obj.material_slots: if not slot.material: slot.material = bpy.data.materials["TT_bake_node"] setup_image_bake_node(obj, bakeReadyMaterials, image_name, previous_image_name, imagecopy_name) # Assign material to highpoly or tune the existing material/s for obj in (bset.objects_high+bset.objects_float): assign_tune_materials(obj) #print("Bake '{}' = {}".format(bset.name, path)) print("Bake "+bset.name) # Hide all cage objects i nrender for obj_cage in bset.objects_cage: obj_cage.hide_render = True # Bake each low poly object in this set for i in range(len(bset.objects_low)): obj_low = bset.objects_low[i] obj_cage = None if i >= len(bset.objects_cage) else bset.objects_cage[i] # Disable hide render obj_low.hide_render = False bpy.ops.object.select_all(action='DESELECT') obj_low.select_set( state = True, view_layer = None) bpy.context.view_layer.objects.active = obj_low # if modes[mode].engine == 'BLENDER_EEVEE': #TODO would this still be needed when the set background code has been moved to the next lines? # # Assign image to texture faces # bpy.ops.object.mode_set(mode='EDIT') # bpy.ops.mesh.select_all(action='SELECT') # for area in bpy.context.screen.areas: # if area.ui_type == 'UV': # area.spaces[0].image = image # # bpy.data.screens['UV Editing'].areas[1].spaces[0].image = image # bpy.ops.object.mode_set(mode='OBJECT') if is_clear and i == 0: # Set background image (CYCLES & BLENDER_EEVEE) # for area in bpy.context.screen.areas: # if area.ui_type == 'UV': # area.spaces[0].image = bpy.data.images[image_name] # Invert background if final invert of the baked image is needed if modes[mode].invert: bpy.ops.image.invert(invert_r=True, invert_g=True, invert_b=True, invert_a=False) for obj_high in (bset.objects_high): obj_high.select_set( state = True, view_layer = None) cycles_bake( mode, bpy.context.scene.texToolsSettings.padding, sampling_scale, samples, cage_extrusion, ray_distance, len(bset.objects_high) > 0, obj_cage ) # Bake Floaters separate bake if len(bset.objects_float) > 0: bpy.ops.object.select_all(action='DESELECT') for obj_high in (bset.objects_float): obj_high.select_set( state = True, view_layer = None) obj_low.select_set( state = True, view_layer = None) cycles_bake( mode, 0, sampling_scale, samples, cage_extrusion, ray_distance, len(bset.objects_float) > 0, obj_cage ) # Restore renderable for cage objects for obj_cage in bset.objects_cage: obj_cage.hide_render = False # Operations to be made only after the bake is -or the bakes are- finished if (not bake_force == "Single") or (bake_force == "Single" and s == len(sets)-1): if modes[mode].invert: bpy.ops.image.invert(invert_r=True, invert_g=True, invert_b=True, invert_a=False) if render_width != size[0] or render_height != size[1]: bpy.data.images[image_name].scale(size[0],size[1]) if modes[mode].composite: apply_composite(image_name, modes[mode].composite, bpy.context.scene.texToolsSettings.bake_curvature_size) # TODO: if autosave: image.save() finally: # Restore materials whether or not there is a problem during the baking for obj in previous_materials: if len(previous_materials[obj]) == 0: if len(obj.material_slots) > 0: obj.active_material_index = 0 context_override = {'object': obj} for i in range(len(obj.material_slots)): with bpy.context.temp_override(**context_override): bpy.ops.object.material_slot_remove() else: for i, mtlname in enumerate(previous_materials[obj]): if mtlname is None: obj.data.materials[i] = None else: obj.data.materials[i] = bpy.data.materials[mtlname] if material_loaded is not None: if modes[mode].setVColor: vclsNames = [vcl.name for vcl in obj.data.vertex_colors] if 'TexTools_temp' in vclsNames : obj.data.vertex_colors.remove(obj.data.vertex_colors['TexTools_temp']) for mtl in copied_materials.values(): bpy.data.materials.remove(bpy.data.materials[mtl], do_unlink=True) if "TT_bake_node" in bpy.data.materials: bpy.data.materials.remove(bpy.data.materials["TT_bake_node"], do_unlink=True) if material_loaded is not None: bpy.data.materials.remove(bpy.data.materials[material_loaded], do_unlink=True) for images in stored_images: if images[1] and images[1] in bpy.data.images and bpy.data.images[images[0]] != bpy.data.images[images[1]]: # If Avoid Circular Dependency method B was used, change previous_image for the newly baked image in all materials #bpy.data.images[images[1]].user_remap(bpy.data.images[images[0]]) for material in bpy.data.materials: if material.use_nodes == True: tree = material.node_tree for node in tree.nodes: if node.bl_idname == 'ShaderNodeTexImage': if node.image == bpy.data.images[images[1]]: if material in copied_materials: circular_report[0] = True node.image = bpy.data.images[images[0]] # Force previous or temporary images to be removed (user related errors may be prompted in console, but the process should be stable) if images[2] and images[2] in bpy.data.images: #bpy.data.images[images[2]].user_clear() #bpy.data.batch_remove([bpy.data.images[images[2]]]) bpy.data.images.remove(bpy.data.images[images[2]], do_unlink=True) # Delete imagecopy elif images[1] and images[1] in bpy.data.images: #bpy.data.images[images[1]].user_clear() #bpy.data.batch_remove([bpy.data.images[images[1]]]) bpy.data.images.remove(bpy.data.images[images[1]], do_unlink=True) # Delete previous_image # If Avoid Circular Dependency method A was used, clear users of the saved image and recover them for the newly baked image (they share the ID but are not the same) if images[2]: circular_report[0] = True bpy.data.images[images[0]].user_clear() for material in bpy.data.materials: if material.use_nodes == True: tree = material.node_tree for node in tree.nodes: if node.bl_idname == 'ShaderNodeTexImage': if node.image.name == images[0]: node.image = bpy.data.images[images[0]] # Always set proper name to the newly baked image (when all previous or temporary images have been removed) bpy.data.images[images[0]].name = images[3] # Restore settings and selection mode ub.restore_bake_settings() bpy.ops.object.select_all(action='DESELECT') for obj in selected: obj.select_set( state = True, view_layer = None) # Enter and exit Edit Mode to force set a real vertex colors layer as active bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.mode_set(mode='OBJECT') if active: bpy.context.view_layer.objects.active = active bpy.ops.object.mode_set(mode=pre_selection_mode) def apply_composite(image_name, scene_name, size): image = bpy.data.images[image_name] previous_scene = bpy.context.window.scene # avoid Sun Position addon error preWorldPropertiesNodesBool = previous_scene.world.use_nodes # Get Scene with compositing nodes scene = None if scene_name in bpy.data.scenes: scene = bpy.data.scenes[scene_name] else: path = os.path.join(os.path.dirname(__file__), "resources/compositing.blend", "Scene") bpy.ops.wm.append(filename=scene_name, directory=path, link=False, autoselect=False) scene = bpy.data.scenes[scene_name] if scene: # Switch scene bpy.context.window.scene = scene bpy.context.window.scene.world.use_nodes = preWorldPropertiesNodesBool #Setup composite nodes for Curvature if "Image" in scene.node_tree.nodes: scene.node_tree.nodes["Image"].image = image if "Offset" in scene.node_tree.nodes: scene.node_tree.nodes["Offset"].outputs[0].default_value = size # Render image bpy.ops.render.render(use_viewport=False) # Get last images of viewer node and render result image_viewer_node = get_last_item("Viewer Node", bpy.data.images) image_render_result = get_last_item("Render Result", bpy.data.images) #Copy pixels image.pixels = image_viewer_node.pixels[:] image.update() if image_viewer_node: bpy.data.images.remove(image_viewer_node) if image_render_result: bpy.data.images.remove(image_render_result) #Restore scene & remove other scene bpy.context.window.scene = previous_scene # Delete compositing scene bpy.data.scenes.remove(scene) def get_last_item(key_name, collection): # bpy.data.images # Get last image of a series, e.g. .001, .002, 003 keys = [] for item in collection: if key_name in item.name: keys.append(item.name) print("Search for {}x : '{}'".format(len(keys), ",".join(keys) ) ) if len(keys) > 0: return collection[keys[-1]] return None def setup_image(color_report, mode, name, width, height, tiles, material_load=False): preferences = bpy.context.preferences.addons[__package__].preferences if preferences.bool_bake_back_color == 'CUSTOM': bake_back_color = bpy.context.scene.texToolsSettings.bake_back_color else: bake_back_color = modes[mode].color def set_color_space(color_report, image): image.alpha_mode = 'NONE' try: image.colorspace_settings.name = bpy.context.scene.texToolsSettings.bake_color_space except: try: if bpy.context.scene.texToolsSettings.bake_color_space == 'Utility - Linear - sRGB': bpy.context.scene.texToolsSettings.bake_color_space = 'Non-Color' color_report[0] = "ACES Color Space type is not available" elif bpy.context.scene.texToolsSettings.bake_color_space == 'Utility - sRGB - Texture': bpy.context.scene.texToolsSettings.bake_color_space = 'sRGB' color_report[0] = "ACES Color Space type is not available" elif bpy.context.scene.texToolsSettings.bake_color_space == 'Non-Color': bpy.context.scene.texToolsSettings.bake_color_space = 'Utility - Linear - sRGB' color_report[0] = "Standard RGB Color Space type is not available" elif bpy.context.scene.texToolsSettings.bake_color_space == 'sRGB': bpy.context.scene.texToolsSettings.bake_color_space = 'Utility - sRGB - Texture' color_report[0] = "Standard RGB Color Space type is not available" image.colorspace_settings.name = bpy.context.scene.texToolsSettings.bake_color_space except: color_report[0] = "No one of the known Color Space types is available" return None def resize(image): if image.size[0] != width or image.size[1] != height or image.generated_width != width or image.generated_height != height: image.scale(width, height) def set_image_as_background(image): for area in bpy.context.screen.areas: if area.ui_type == 'UV': area.spaces[0].image = bpy.data.images[image.name] def apply_color(image): # Set background color to a small version of the image for performance image.pixels = [pv for p in range(4) for pv in bake_back_color] # Set final size of the image resize(image) def image_create(): is_float_32 = preferences.bake_32bit_float == '32' if tiles and settings.bversion >= 3.2: # Create a full size new tiled image with alpha background image = bpy.data.images.new(name, width=width, height=height, alpha=True, float_buffer=is_float_32, tiled=True) else: # Create a small new image image = bpy.data.images.new(name, width=2, height=2, alpha=True, float_buffer=is_float_32, tiled=False) set_image_as_background(image) set_color_space(color_report, image) if tiles and settings.bversion >= 3.2: image.tiles.get(1001).generated_color = bake_back_color for tile in tiles: bpy.ops.image.tile_add(number=tile, count=1, label="", fill=True, width=width, height=height, float=is_float_32, alpha=True, color=bake_back_color) image.tiles.active_index = 0 else: apply_color(image) #TODO revisit this if image save is implemented #image.file_format = 'TARGA' return image if name in bpy.data.images: previous_image = bpy.data.images[name] if previous_image.source == 'FILE': # Clear image if it was deleted or moved outside print("Existing image expected path "+bpy.path.abspath(previous_image.filepath)) if not os.path.isfile(bpy.path.abspath(previous_image.filepath)): print("Unlinking missing image "+name) image = image_create() if material_load: return image, None # Not possible Circular Dependency return image, previous_image # Avoid Circular Dependency: use method B else: set_image_as_background(previous_image) set_color_space(color_report, previous_image) if settings.bversion < 3.2 or not tiles: previous_image.scale(2, 2) apply_color(previous_image) if material_load: return previous_image, None # Not possible Circular Dependency return previous_image, previous_image # Avoid Circular Dependency: use method A else: if material_load: set_image_as_background(previous_image) set_color_space(color_report, previous_image) if settings.bversion < 3.2 or not tiles: previous_image.scale(2, 2) apply_color(previous_image) return previous_image, None # Not possible Circular Dependency image = image_create() return image, previous_image # Avoid Circular Dependency: use method B else: image = image_create() return image, None # Not possible Circular Dependency def setup_image_bake_node(obj, bakeReadyMaterials, image_name, previous_image_name, imagecopy_name): image = bpy.data.images[image_name] if previous_image_name: previous_image = bpy.data.images[previous_image_name] else: previous_image = None if imagecopy_name: imagecopy = bpy.data.images[imagecopy_name] else: imagecopy = None if len(obj.data.materials) <= 0: print("ERROR, need spare material to setup active image texture to bake!!!") else: def avoid_circular(tree, image): # Avoid Circular Dependency method A for node in tree.nodes: if node.bl_idname == 'ShaderNodeTexImage': if node.image == image: node.image = imagecopy def assign_node(tree): # Assign bake node node = None if "TexTools_bake" in tree.nodes: node = tree.nodes["TexTools_bake"] else: node = tree.nodes.new("ShaderNodeTexImage") node.name = "TexTools_bake" node.select = True node.image = image tree.nodes.active = node for slot in obj.material_slots: if slot.material: if slot.material.name not in bakeReadyMaterials: if image == previous_image: if slot.material.use_nodes == False: # No need to Avoid Circular Dependency slot.material.use_nodes = True assign_node(slot.material.node_tree) else: # Use Avoid Circular Dependency method A to preserve external linking of the existing previous_image avoid_circular(slot.material.node_tree, image) assign_node(slot.material.node_tree) else: # Avoid Circular Dependency method B is used just by not baking directly on the existing previous_image if slot.material.use_nodes == False: slot.material.use_nodes = True assign_node(slot.material.node_tree) bakeReadyMaterials.append(slot.material.name) def relink_nodes(mode, material): if material.use_nodes == False: material.use_nodes = True tree = material.node_tree bsdf_node = None for n in tree.nodes: if n.bl_idname == "ShaderNodeBsdfPrincipled": bsdf_node = n # set b, which is the base(original) socket index, and n, which is the new-values-source index for the base socket b, n = modes[mode].relink['b'], modes[mode].relink['n'] base_node = base_socket = None if len(bsdf_node.inputs[b].links) != 0 : base_node = bsdf_node.inputs[b].links[0].from_node base_socket = bsdf_node.inputs[b].links[0].from_socket.name base_value = (bsdf_node.inputs[b].default_value, ) # If the base value is a color, decompose its value so it can be stored and recovered later, otherwise its value will change while the swap of sockets is committed if not isinstance(base_value[0], float): base_value = ((base_value[0][0],base_value[0][1],base_value[0][2],base_value[0][3]), ) new_node = None if len(bsdf_node.inputs[n].links) != 0 : new_node = bsdf_node.inputs[n].links[0].from_node new_node_socket = bsdf_node.inputs[n].links[0].from_socket.name if (new_node == base_node and new_node != None) and base_socket == new_node_socket : pass else: bsdf_node.inputs[b].default_value = bsdf_node.inputs[n].default_value tree.links.new(new_node.outputs[new_node_socket], bsdf_node.inputs[b]) else: if base_node: tree.links.remove(bsdf_node.inputs[b].links[0]) bsdf_node.inputs[b].default_value = bsdf_node.inputs[n].default_value def channel_ignore(channel, material): if material.use_nodes == False: material.use_nodes = True tree = material.node_tree bsdf_node = None for n in tree.nodes: if n.bl_idname == "ShaderNodeBsdfPrincipled": bsdf_node = n if len(bsdf_node.inputs[channel].links) != 0 : tree.links.remove(bsdf_node.inputs[channel].links[0]) # So far, Channels whose effect on others is wanted to be ignored have to be set equal to 1.0 bsdf_node.inputs[channel].default_value = 1.0 def setup_material_loaded(mode, name): material_bake = bpy.data.materials[name] if mode == 'wireframe': if "Value" in material_bake.node_tree.nodes: material_bake.node_tree.nodes["Value"].outputs[0].default_value = bpy.context.scene.texToolsSettings.bake_wireframe_size if mode == 'bevel_mask': if "Bevel" in material_bake.node_tree.nodes: material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples if mode == 'normal_tangent_bevel': if "Bevel" in material_bake.node_tree.nodes: material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples if mode == 'normal_object_bevel': if "Bevel" in material_bake.node_tree.nodes: material_bake.node_tree.nodes["Bevel"].inputs[0].default_value = bpy.context.scene.texToolsSettings.bake_bevel_size material_bake.node_tree.nodes["Bevel"].samples = bpy.context.scene.texToolsSettings.bake_bevel_samples if mode == 'thickness': if "ao" in material_bake.node_tree.nodes: material_bake.node_tree.nodes["ao"].samples = bpy.context.scene.texToolsSettings.bake_samples material_bake.node_tree.nodes["ao"].only_local = bpy.context.scene.texToolsSettings.bake_thickness_local if "Distance" in material_bake.node_tree.nodes: material_bake.node_tree.nodes["Distance"].outputs[0].default_value = bpy.context.scene.texToolsSettings.bake_thickness_distance if "Contrast" in material_bake.node_tree.nodes: material_bake.node_tree.nodes["Contrast"].outputs[0].default_value = bpy.context.scene.texToolsSettings.bake_thickness_contrast def get_material(mode): if modes[mode].material == "": return None # No material setup required # Find or load material name = modes[mode].material path = os.path.join(os.path.dirname(__file__), "resources/materials.blend", "Material") if "bevel" in mode or "thickness" in mode: path = os.path.join(os.path.dirname(__file__), "resources/materials_2.80.blend", "Material") #print("Get material {}\n{}".format(mode, path)) if bpy.data.materials.get(name) is None: #print("Material not yet loaded: "+mode) bpy.ops.wm.append(filename=name, directory=path, link=False, autoselect=False) return name def cycles_bake(mode, padding, sampling_scale, samples, cage_extrusion, ray_distance, is_multi, obj_cage): # if modes[mode].engine == 'BLENDER_EEVEE': # # Snippet: https://gist.github.com/AndrewRayCode/760c4634a77551827de41ed67585064b # bpy.context.scene.render.bake_margin = padding # # AO Settings # bpy.context.scene.render.bake_type = modes[mode].type # bpy.context.scene.render.use_bake_normalize = True # if modes[mode].type == 'AO': # bpy.context.scene.world.light_settings.use_ambient_occlusion = True # bpy.context.scene.world.light_settings.gather_method = 'RAYTRACE' # bpy.context.scene.world.light_settings.samples = samples # bpy.context.scene.render.use_bake_selected_to_active = is_multi # bpy.context.scene.render.bake_distance = ray_distance # bpy.context.scene.render.use_bake_clear = False # bpy.ops.object.bake_image() if modes[mode].engine == 'CYCLES' or modes[mode].engine == 'BLENDER_EEVEE' : if modes[mode].normal_space == 'OBJECT': #See: https://twitter.com/Linko_3D/status/963066705584054272 bpy.context.scene.render.bake.normal_r = 'POS_X' bpy.context.scene.render.bake.normal_g = 'POS_Z' bpy.context.scene.render.bake.normal_b = 'NEG_Y' elif modes[mode].normal_space == 'TANGENT': bpy.context.scene.render.bake.normal_r = 'POS_X' bpy.context.scene.render.bake.normal_b = 'POS_Z' # Adjust Y swizzle from Addon preferences swizzle_y = bpy.context.preferences.addons[__package__].preferences.swizzle_y_coordinate if swizzle_y == 'Y-': bpy.context.scene.render.bake.normal_g = 'NEG_Y' elif swizzle_y == 'Y+': bpy.context.scene.render.bake.normal_g = 'POS_Y' # Set samples bpy.context.scene.cycles.samples = samples # Speed up samples for simple render modes if modes[mode].type in {'EMIT', 'DIFFUSE', 'ROUGHNESS', 'TRANSMISSION', 'ENVIRONMENT', 'UV'} and mode != 'thickness': bpy.context.scene.cycles.samples = 1 # Pixel Padding bpy.context.scene.render.bake.margin = padding * sampling_scale # Disable Direct and Indirect for all 'DIFFUSE' bake types if modes[mode].type == 'DIFFUSE': bpy.context.scene.render.bake.use_pass_direct = False bpy.context.scene.render.bake.use_pass_indirect = False bpy.context.scene.render.bake.use_pass_color = True if settings.bversion < 2.90: if obj_cage is None: bpy.ops.object.bake( type=modes[mode].type, use_clear=False, use_selected_to_active=is_multi, cage_extrusion=cage_extrusion, normal_space=modes[mode].normal_space ) else: bpy.ops.object.bake( type=modes[mode].type, use_clear=False, use_selected_to_active=is_multi, cage_extrusion=cage_extrusion, normal_space=modes[mode].normal_space, use_cage=True, cage_object=obj_cage.name ) else: if obj_cage is None: bpy.ops.object.bake( type=modes[mode].type, use_clear=False, use_selected_to_active=is_multi, cage_extrusion=cage_extrusion, max_ray_distance=ray_distance, normal_space=modes[mode].normal_space ) else: bpy.ops.object.bake( type=modes[mode].type, use_clear=False, use_selected_to_active=is_multi, cage_extrusion=cage_extrusion, max_ray_distance=ray_distance, normal_space=modes[mode].normal_space, use_cage=True, cage_object=obj_cage.name ) bpy.utils.register_class(op)