import bpy from bpy.types import Operator from bpy.props import StringProperty, BoolProperty from bpy.utils import register_class, unregister_class from .utils import SBConstants from .messages import print_message from .aov import get_aov_name class SimpleBake_OT_PrepareObjectMatsPbr(Operator): """Prepare an object for PBR baking""" bl_idname = "simplebake.prepare_object_mats_pbr" bl_description = "Prepare object for PBR bake" bl_label = "Prepare" target_name: StringProperty() this_bake: StringProperty() bake_operation_id: StringProperty() no_bake_image_node: BoolProperty(default=False) only_bake_image_node: BoolProperty(default=False) def create_dummy_nodes(self, context, mat): print_message(context, f"Creating dummy nodes for {self.target_name}") node_tree = mat.node_tree nodes = node_tree.nodes p_nodes = [n for n in nodes if n.bl_idname == "ShaderNodeBsdfPrincipled"] #Special case for bump if self.this_bake == SBConstants.PBR_BUMP: for p_node in p_nodes: #Nothing connected if len(p_node.inputs["Normal"].links) == 0: p_node["SB_Ignore_Normal"]= True continue #Connected but not a bump node bump_node = p_node.inputs["Normal"].links[0].from_node if bump_node.bl_idname != "ShaderNodeBump": p_node["SB_Ignore_Normal"]= True continue #Nothing connected to bump node height input if len(bump_node.inputs['Height'].links) == 0: p_node["SB_Ignore_Normal"]= True continue #Must be OK to be treated normally #p_node.inputs["Normal"]["SB_Ignore"] = False #if len(bump_node.inputs[2].links) == 0: #val = bump_node.inputs[2].default_value #vnode = nodes.new("ShaderNodeValue") #vnode.outputs[0].default_value = val #vnode.label = "SimpleBake" #node_tree.links.new(vnode.outputs[0], bump_node.inputs[2]) #Normal case, not bump else: socket_name = SBConstants.get_socket_names()[self.this_bake] for p_node in p_nodes: #Get the sockets we care about (correct name + empty) sockets = [s for s in p_node.inputs if s.name == socket_name and len(s.links)==0] for socket in sockets: #If not, get value of the unconnected socket val = socket.default_value socket_type = socket.bl_idname #if socket_type == "NodeSocketColor": #Not needed, but future proofing if socket_type.startswith("NodeSocketColor"): rgb = nodes.new("ShaderNodeRGB") rgb.outputs[0].default_value = val rgb.label = "SimpleBake" #???????????????????????????? node_tree.links.new(rgb.outputs[0], socket) #if socket_type in ["NodeSocketFloat", "NodeSocketFloatFactor", "NodeSocketFloatDistance"]: if socket_type.startswith("NodeSocketFloat"): vnode = nodes.new("ShaderNodeValue") vnode.outputs[0].default_value = val vnode.label = "SimpleBake" #????????????????????????????????????????? node_tree.links.new(vnode.outputs[0], socket) def bypass_muted_mix_shaders(self, mat): #Let's bypass all the muted mix shader nodes node_tree = mat.node_tree nodes = node_tree.nodes for n in nodes: if n.bl_idname == "ShaderNodeMixShader" and n.mute: if len(n.inputs[1].links) > 0: fs = n.inputs[1].links[0].from_socket else: fs = False if len(n.outputs[0].links) > 0: ts = n.outputs[0].links[0].to_socket else: ts = False if fs and ts: node_tree.links.new(fs, ts) nodes.remove(n) def swap_e_for_p(self,mat): node_tree = mat.node_tree nodes = node_tree.nodes enodes = [n for n in nodes if n.bl_idname == "ShaderNodeEmission"] for e in enodes: cval = e.inputs[0].default_value sval = e.inputs[1].default_value pos = e.location if len(e.inputs[0].links) > 0: cfs = e.inputs[0].links[0].from_socket else: cfs = False if len(e.inputs[1].links) > 0: sfs = e.inputs[1].links[0].from_socket else: sfs = False os = e.outputs[0].links[0].to_socket #Create principled BSDF p = nodes.new("ShaderNodeBsdfPrincipled") if cfs: node_tree.links.new(cfs, p.inputs["Emission Color"]) else: p.inputs["Emission Color"].default_value = cval if sfs: node_tree.links.new(sfs, p.inputs["Emission Strength"]) else: p.inputs["Emission Strength"].default_value = sval p.inputs["Base Color"].default_value = (0.0,0.0,0.0,1.0) node_tree.links.new(p.outputs[0], os) p.location = pos #Last action, remove all the e nodes [nodes.remove(n) for n in enodes] def remove_disconnected(self, context, mat): print_message(context, f"Removing disconnected nodes from {mat.name}") nodes = mat.node_tree.nodes #Start with the material output node #Remove any not in use. So not to confuse matters. [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and len(n.inputs[0].links)==0] #Find the active material output mos = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == True] assert(len(mos) == 1) active_mo = mos[0] #Is it Cycles? if active_mo.target in ["CYCLES", "ALL"]: #Yes. So we are good. Just remove the others mos = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == False] else: #Active not Cycles. Can we find a pre-existing Cycles one somewhere? mos = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.target in ["CYCLES","ALL"]] if len(mos)>0: #Found existing Cycles one, make it active and remove the others mos[0].is_active_output = True #Set to All, just for the viewport candy mos[0].target = "ALL" [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == False] else: #Active isn't Cycles, and no Cycles in the material. Pick one at random. Force to Cycles mos = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial"] mos[0].is_active_output = True mos[0].target = "ALL" [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output == False] #Loop through nodes to remove disconnected nodes that we care about repeat = False r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeBsdfPrincipled" and len(n.outputs[0].links) == 0 ] if len(r) >0: repeat = True #We removed something r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeEmission" and len(n.outputs[0].links) == 0 ] if len(r) >0: repeat = True #We removed something r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeMixShader" and len(n.outputs[0].links) == 0 ] if len(r) >0: repeat = True #We removed something #Special case for Shader to RGB node r = [nodes.remove(n) for n in nodes if n.bl_idname == "ShaderNodeShaderToRGB" and len(n.outputs[0].links) == 0 ] if len(r) >0: repeat = True #We removed something #If we removed any nodes, we need to do this again if repeat: self.remove_disconnected(context, mat) def get_active_output_node(self, mat): nodes = mat.node_tree.nodes output_nodes = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial"] assert(len(output_nodes)==1) return output_nodes[0] def get_surface_shader_node(self, mat): output_node = self.get_active_output_node(mat) surface = output_node.inputs.get("Surface") if surface == None or len(surface.links) == 0: return None return surface.links[0].from_node def setup_pure_p_mat(self, context, mat): print_message(context, f"Setup for pure P material for {self.target_name}") node_tree = mat.node_tree nodes = node_tree.nodes output_node = self.get_active_output_node(mat) p_node = self.get_surface_shader_node(mat) if p_node == None or p_node.bl_idname != "ShaderNodeBsdfPrincipled": print_message(context, f"Material {mat.name} is not a pure Principled material") return False #Create an emission shader and connect to output emissnode = nodes.new("ShaderNodeEmission") emissnode.label = "SimpleBake" #?????????????????????????????????? emissnode.location = (output_node.location.x, output_node.location.y+200) node_tree.links.new(emissnode.outputs[0], output_node.inputs[0]) #Connect whatever is in Principled Shader for this bakemode to the emission if self.this_bake == SBConstants.PBR_BUMP: normal_socket = p_node.inputs["Normal"] if "SB_Ignore_Normal" in p_node: emissnode.inputs[0].default_value = (0.5,0.5,0.5,1) else: bump_node = p_node.inputs["Normal"].links[0].from_node bump_input = bump_node.inputs['Height'].links[0].from_socket node_tree.links.new(bump_input, emissnode.inputs[0]) else: socket_name = SBConstants.get_socket_names()[self.this_bake] socket = p_node.inputs[socket_name] node_tree.links.new(socket.links[0].from_socket, emissnode.inputs[0]) return True def setup_mix_mat(self, context, mat): print_message(context, f"Setup for mix material for {self.target_name}") node_tree = mat.node_tree nodes = node_tree.nodes output_node = self.get_active_output_node(mat) surface_node = self.get_surface_shader_node(mat) if surface_node == None or surface_node.bl_idname != "ShaderNodeMixShader": print_message(context, f"Material {mat.name} is not a Mix Shader material") return False #For every mix shader, create a mixrgb above it mix_nodes = [n for n in nodes if n.bl_idname == "ShaderNodeMixShader"] for mix_node in mix_nodes: #Create RGB mix for every mix shader node rgbmix = nodes.new("ShaderNodeMixRGB") rgbmix.location = (mix_node.location.x, mix_node.location.y+200) rgbmix.label="SimpleBake" #???????????????????????????????????????? rgbmix["SB_rgb_proxy_for"] = mix_node.name #If there is one, plug the factor from the original mix node into our new mix node if len(mix_node.inputs[0].links) > 0: node_tree.links.new(mix_node.inputs[0].links[0].from_socket, rgbmix.inputs[0]) #Else, copy the default value else: rgbmix.inputs[0].default_value = mix_node.inputs[0].default_value #Loop over the new RGBmix nodes rgbmix_nodes = [n for n in nodes if "SB_rgb_proxy_for" in n] assert(len(rgbmix_nodes)>0) for rgbmix_node in rgbmix_nodes: #Get the original mix node mix_node = nodes[rgbmix_node["SB_rgb_proxy_for"]] #Socket 1 (back to p_node) if len(mix_node.inputs[1].links)>0 and mix_node.inputs[1].links[0].from_node.bl_idname == "ShaderNodeBsdfPrincipled": origin_p_node = mix_node.inputs[1].links[0].from_node if self.this_bake == SBConstants.PBR_BUMP: normal_socket = origin_p_node.inputs["Normal"] if "SB_Ignore_Normal" in origin_p_node: rgbmix_node.inputs[1].default_value = (0.5,0.5,0.5,1) else: bump_node = origin_p_node.inputs["Normal"].links[0].from_node bump_input = bump_node.inputs['Height'].links[0].from_socket node_tree.links.new(bump_input, rgbmix_node.inputs[1]) else: socket_name = SBConstants.get_socket_names()[self.this_bake] origin_socket = origin_p_node.inputs[socket_name].links[0].from_socket node_tree.links.new(origin_socket, rgbmix_node.inputs[1]) #Socket 2 (back to p_node) if len(mix_node.inputs[2].links)>0 and mix_node.inputs[2].links[0].from_node.bl_idname == "ShaderNodeBsdfPrincipled": origin_p_node = mix_node.inputs[2].links[0].from_node if self.this_bake == SBConstants.PBR_BUMP: normal_socket = origin_p_node.inputs["Normal"] if "SB_Ignore_Normal" in origin_p_node: rgbmix_node.inputs[2].default_value = (0.5,0.5,0.5,1) else: bump_node = origin_p_node.inputs["Normal"].links[0].from_node bump_input = bump_node.inputs['Height'].links[0].from_socket node_tree.links.new(bump_input, rgbmix_node.inputs[2]) else: socket_name = SBConstants.get_socket_names()[self.this_bake] origin_socket = origin_p_node.inputs[socket_name].links[0].from_socket node_tree.links.new(origin_socket, rgbmix_node.inputs[2]) #Socket 1 (back to mix node) if len(mix_node.inputs[1].links)>0 and mix_node.inputs[1].links[0].from_node.bl_idname == "ShaderNodeMixShader": origin_mix_node = mix_node.inputs[1].links[0].from_node #Get the RGB mix for that origin mix node origin_rgbmix = [n for n in nodes if "SB_rgb_proxy_for" in n and n["SB_rgb_proxy_for"] == origin_mix_node.name] assert(len(origin_rgbmix) == 1) origin_rgbmix = origin_rgbmix[0] node_tree.links.new(origin_rgbmix.outputs[0], rgbmix_node.inputs[1]) #Socket 2 (back to mix node) if len(mix_node.inputs[2].links)>0 and mix_node.inputs[2].links[0].from_node.bl_idname == "ShaderNodeMixShader": origin_mix_node = mix_node.inputs[2].links[0].from_node #Get the RGB mix for that origin mix node origin_rgbmix = [n for n in nodes if "SB_rgb_proxy_for" in n and n["SB_rgb_proxy_for"] == origin_mix_node.name] assert(len(origin_rgbmix) == 1) origin_rgbmix = origin_rgbmix[0] node_tree.links.new(origin_rgbmix.outputs[0], rgbmix_node.inputs[2]) #Back to nowhere if len(mix_node.inputs[1].links) == 0: rgbmix_node.inputs[1].default_value = (0,0,0,1) if len(mix_node.inputs[2].links) == 0: rgbmix_node.inputs[2].default_value = (0,0,0,1) #Finally connect to the output final_mix = output_node.inputs[0].links[0].from_node final_rgbmix = [n for n in nodes if "SB_rgb_proxy_for" in n and n["SB_rgb_proxy_for"] == final_mix.name] assert(len(final_rgbmix)>0) final_rgbmix = final_rgbmix[0] node_tree.links.new(final_rgbmix.outputs[0], output_node.inputs[0]) return True def setup_displacement(self, context, mat): print_message(context, f"Setup for displacement bake on {self.target_name}") node_tree = mat.node_tree nodes = node_tree.nodes #Find the active MO node mo_node = None for n in nodes: if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output: mo_node = n if mo_node == None: print(f"Error - no active Material Output in material {mat.name}") return False #See if anything is plugged into the Displacement Socket mo_disp_socket = mo_node.inputs["Displacement"] if len(mo_disp_socket.links) <1: print_message(context, f"Skipping. Nothing plugged into displacement socket for material {mat.name}") return True #Find the from node. Check it's either Displacement or Displacement Vector disp_node = mo_disp_socket.links[0].from_node if disp_node.bl_idname not in ["ShaderNodeDisplacement", "ShaderNodeVectorDisplacement"]: return False #Found disp node source_socket = None socket_name = "Height" if disp_node.bl_idname == "ShaderNodeDisplacement" else "Vector" #Is something plugged into Height/Vector?? if len(disp_node.inputs[socket_name].links)>0: source_socket = disp_node.inputs[socket_name].links[0].from_socket else: #NOTHING PLUGGED IN if disp_node.inputs[socket_name].bl_idname == "NodeSocketColor": #No dummy node possible return False else: value_node = nodes.new(type='ShaderNodeValue') value_node.outputs[0].default_value = disp_node.inputs[socket_name].default_value source_socket = value_node.outputs[0] if source_socket == None: return False #Create emission node and hook it up emission_node = nodes.new(type='ShaderNodeEmission') emission_node.location = (mo_node.location.x, mo_node.location.y+150) node_tree.links.new(source_socket, emission_node.inputs["Color"]) node_tree.links.new(emission_node.outputs[0], mo_node.inputs["Surface"]) def setup_aov(self, context, mat, aovname): print_message(context, f"Setup for AOV bake for {aovname}") node_tree = mat.node_tree nodes = node_tree.nodes aov_node = None #Find the AOV node that we want for n in nodes: if n.bl_idname == "ShaderNodeOutputAOV": if n.aov_name == aovname: #Found it aov_node = n #Give up if we can't find the AOV node we want (for whatver reason) if aov_node == None: return False #Find the active MO node mo_node = None for n in nodes: if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output: mo_node = n assert(mo_node!=None) #Col is prioritised to fit with how vanilla Blender does it if len(aov_node.inputs['Color'].links)>0: source_socket = aov_node.inputs['Color'].links[0].from_socket elif len(aov_node.inputs['Value'].links)>0: source_socket = aov_node.inputs['Value'].links[0].from_socket else: print_message(context, "ERROR - Couldn't find source of AOV node") return False #Create emission node and hook it up emission_node = nodes.new(type='ShaderNodeEmission') emission_node.location = (mo_node.location.x, mo_node.location.y+150) node_tree.links.new(source_socket, emission_node.inputs["Color"]) node_tree.links.new(emission_node.outputs[0], mo_node.inputs["Surface"]) def create_bake_image_node(self, mat, mat_name=""): node_tree = mat.node_tree nodes = node_tree.nodes existing_bake_nodes = [n for n in nodes if "SB_bake_image_node" in n] if len(existing_bake_nodes) !=0: #Just quit if there is already a node in this material #e.g. Specials are used more than once. Also object may have same PBR material multiple times return True image_node = nodes.new("ShaderNodeTexImage") #Where is the image we want? This is different for a merged bake or per-mat bake if not self.merged_bake: if mat_name: # tex_per_mat: find the image specific to this material image_name = ([i.name for i in bpy.data.images if i.get("SB_bake_object") == self.target_name and i.get("SB_bake_operation_id") == self.bake_operation_id and i.get("SB_this_bake") == self.this_bake and i.get("SB_bake_material") == mat_name ]) else: image_name = ([i.name for i in bpy.data.images if "SB_bake_object" in i and i["SB_bake_object"] == self.target_name and "SB_bake_operation_id" in i and i["SB_bake_operation_id"] == self.bake_operation_id and "SB_this_bake" in i and i["SB_this_bake"] == self.this_bake ]) else: image_name = ([i.name for i in bpy.data.images if "SB_bake_operation_id" in i and i["SB_bake_operation_id"] == self.bake_operation_id and "SB_this_bake" in i and i["SB_this_bake"] == self.this_bake ]) assert len(image_name)==1, f"Found {len(image_name)} images looking for {self.this_bake}" image_name = image_name[0] image_node.image = bpy.data.images[image_name] #Leave it the only node selected for n in nodes: n.select = False image_node.select = True nodes.active = image_node image_node["SB_bake_image_node"] = True return True def create_dummy_bake_image_node(self, mat): """Create a throwaway 1x1 image node for excluded materials so Blender has somewhere to write their pixels (output is discarded).""" node_tree = mat.node_tree nodes = node_tree.nodes existing_bake_nodes = [n for n in nodes if "SB_bake_image_node" in n] if existing_bake_nodes: return dummy_name = "SB_excluded_mat_dummy" if dummy_name not in bpy.data.images: dummy = bpy.data.images.new(dummy_name, 1, 1, alpha=False) dummy["SB_dummy"] = True else: dummy = bpy.data.images[dummy_name] image_node = nodes.new("ShaderNodeTexImage") image_node.image = dummy for n in nodes: n.select = False image_node.select = True nodes.active = image_node image_node["SB_bake_image_node"] = True def execute(self, context): sbp = context.scene.SimpleBake_Props self.merged_bake = sbp.merged_bake mats=[] if not (o:= context.scene.objects.get(self.target_name)): print_message(context, f"ERROR: Preparing object. Couldn't find {self.target_name}!") return {'CANCELLED'} else: #Get list of materials for target object mats = [slot.material for slot in o.material_slots if slot.material!=None] # Build per-material enabled/disabled sets from mat_bake_list (empty = no restrictions) # Working backup materials have an SBW_ prefix — strip it to get the original name mat_entries = [e for e in sbp.mat_bake_list if e.obj_name == self.target_name] enabled_mat_names = {e.mat_name for e in mat_entries if e.enabled} disabled_mat_names = {e.mat_name for e in mat_entries if not e.enabled} has_restrictions = bool(mat_entries) # any mat_bake_list entries for this object def orig_name(mat): """Return the original material name, stripping SBW_ (working) and SBM_ (master) backup prefixes.""" name = mat.name if name.startswith("SBW_"): name = name[4:] if name.startswith("SBM_"): name = name[4:] return name #No prep needed for normal or any specials. Just create bake image node (if that) if (self.this_bake == SBConstants.PBR_NORMAL) or (self.this_bake in SBConstants.ALL_SPECIALS): print_message(context, "No object prep needed - specials or normal map") if not self.no_bake_image_node: for mat in mats: oname = orig_name(mat) if self.this_bake in SBConstants.ALL_SPECIALS and "SB_original_mat_name" in mat: oname = mat["SB_original_mat_name"] if has_restrictions and oname in disabled_mat_names: self.create_dummy_bake_image_node(mat) else: use_per_mat = sbp.tex_per_mat and not self.merged_bake and has_restrictions bake_mat_name = oname if use_per_mat else "" self.create_bake_image_node(mat, mat_name=bake_mat_name) mat["SB_bake_config"] = self.this_bake return {'FINISHED'} #Otherwise, prepare each material for this bake type for mat in mats: oname = orig_name(mat) # Handle excluded materials if has_restrictions and oname in disabled_mat_names: print_message(context, f"Material {mat.name} is excluded - creating dummy node") self.create_dummy_bake_image_node(mat) continue if "SB_bake_config" in mat and mat["SB_bake_config"] == self.this_bake: print_message(context, f"Material {mat.name} is already in correct config for {self.this_bake}") continue # mat_name to use when looking up per-material image (use original name, not working backup name) # Only use per-mat lookup if this object has mat_bake_list entries (i.e. it's a source object, # not an S2A target object which won't have any entries) bake_mat_name = oname if (sbp.tex_per_mat and not self.merged_bake and has_restrictions) else "" if self.only_bake_image_node: print_message(context, f"Just creating bake image node in mat {mat}") self.create_bake_image_node(mat, mat_name=bake_mat_name) continue #Special case for displacement. Don't need to mess with outher nodes if self.this_bake == SBConstants.PBR_DISPLACEMENT: self.setup_displacement(context, mat) if not self.no_bake_image_node: self.create_bake_image_node(mat, mat_name=bake_mat_name) mat["SB_bake_config"] = self.this_bake #TODO-----------------CHECK FOR ISSUES continue #Special case for AOVs. Don't need to mess with outher nodes print(f"--------------- {self.this_bake}") if SBConstants.PBRAOVS in self.this_bake: aovname = get_aov_name(context, self.this_bake) self.setup_aov(context, mat, aovname) if not self.no_bake_image_node: self.create_bake_image_node(mat, mat_name=bake_mat_name) mat["SB_bake_config"] = self.this_bake continue #Bypass muted mix shaders self.bypass_muted_mix_shaders(mat) #Remove disconnected nodes self.remove_disconnected(context, mat) #Swap all e for p self.swap_e_for_p(mat) #What kind of material is this? node_tree = mat.node_tree nodes = node_tree.nodes surface_node = self.get_surface_shader_node(mat) if surface_node != None and surface_node.bl_idname == "ShaderNodeMixShader": print_message(context, "This is a Mix Material") self.create_dummy_nodes(context, mat) if self.setup_mix_mat(context, mat): mat["SB_bake_config"] = self.this_bake #TODO-----------------CHECK FOR ISSUES elif(surface_node != None and surface_node.bl_idname == "ShaderNodeBsdfPrincipled"): #print_message(context, "This is a Pure P material") self.create_dummy_nodes(context, mat) if self.setup_pure_p_mat(context, mat): mat["SB_bake_config"] = self.this_bake #TODO-----------------CHECK FOR ISSUES else: print_message(context, "Non-PBR material type - will still create bake image node") if not self.no_bake_image_node: self.create_bake_image_node(mat, mat_name=bake_mat_name) return {'FINISHED'} classes = ([ SimpleBake_OT_PrepareObjectMatsPbr ]) def register(): global classes for cls in classes: register_class(cls) def unregister(): global classes for cls in classes: unregister_class(cls)