821 lines
37 KiB
Python
821 lines
37 KiB
Python
import bpy
|
|
from bpy.types import Operator
|
|
from bpy.utils import register_class, unregister_class
|
|
|
|
from .utils import show_message_box, clean_file_name, SBConstants, specials_selection_to_list, pbr_selections_to_list, blender_refresh, get_cached_udim_tiles
|
|
from .messages import print_message
|
|
from .ui.objects_list import refresh_bake_objects_list
|
|
from pathlib import Path
|
|
from .utils import can_write_to_location
|
|
from .bake_operators.pbr_bake_support_operators import has_node_groups, convertable_shaders
|
|
from . import __package__ as base_package
|
|
|
|
|
|
|
|
|
|
def check_mat_valid_for_displacement(mat_name, obj_name, messages):
|
|
|
|
mat = bpy.data.materials.get(mat_name)
|
|
if mat == None:
|
|
return True
|
|
|
|
node_tree = mat.node_tree
|
|
nodes = node_tree.nodes
|
|
|
|
valid = False
|
|
|
|
#groups = [n.name for n in this_node_set if n.bl_idname == "ShaderNodeGroup"]
|
|
|
|
#Find our MO node
|
|
mo = [n for n in nodes if n.bl_idname == "ShaderNodeOutputMaterial" and n.is_active_output]
|
|
assert len(mo)==1, "Couldn't find the Material Output node"
|
|
mo = mo[0]
|
|
|
|
#Empty is OK
|
|
if len(mo.inputs["Displacement"].links)==0:
|
|
valid = True
|
|
|
|
if len(mo.inputs["Displacement"].links)>0 and not valid:
|
|
from_node = mo.inputs["Displacement"].links[0].from_node
|
|
|
|
#Check if this is straightforward
|
|
if from_node.bl_idname in ["ShaderNodeDisplacement", "ShaderNodeVectorDisplacement"]:
|
|
valid = True
|
|
|
|
#Maybe it's in a node group
|
|
elif from_node.bl_idname == "ShaderNodeGroup":
|
|
#Recursive search for the nodes we want
|
|
trees = [from_node.node_tree]
|
|
for tree in trees:
|
|
for node in tree.nodes:
|
|
if node.bl_idname == "ShaderNodeGroup":
|
|
trees.append(node.node_tree)
|
|
continue
|
|
|
|
if node.bl_idname in ["ShaderNodeDisplacement", "ShaderNodeVectorDisplacement"]:
|
|
if len(node.outputs[0].links)>0:
|
|
if node.outputs[0].links[0].to_node.bl_idname == "NodeGroupOutput":
|
|
valid = True
|
|
if valid:
|
|
return True
|
|
else:
|
|
messages.append(f"ERROR: Baking Displacement map, but material \"{mat_name}\" on object \"{obj_name}\" has an")
|
|
messages.append("invalid node plugged into the Material Output displacement socket.")
|
|
messages.append("SimpleBake can only bake a single Displacement node or Vector Displacement node")
|
|
messages.append("connected to the Displacement socket of the active Material Output node.")
|
|
messages.append("If such a node is inside a node group, it must be connected directly to the Group Output node.")
|
|
return False
|
|
|
|
|
|
def find_shader_node_editor():
|
|
|
|
found_space = False
|
|
|
|
for screen in bpy.data.screens:
|
|
for area in screen.areas:
|
|
if area.type == 'NODE_EDITOR':
|
|
for space in area.spaces:
|
|
if space.type == 'NODE_EDITOR' and space.tree_type == 'ShaderNodeTree':
|
|
found_space = True
|
|
break
|
|
if found_space:
|
|
break
|
|
if found_space:
|
|
break
|
|
|
|
|
|
return found_space
|
|
|
|
|
|
def check_for_connected_viewer_node(context, mat):
|
|
sbp = context.scene.SimpleBake_Props
|
|
|
|
mat.use_nodes = True
|
|
|
|
node_tree = mat.node_tree
|
|
nodes = node_tree.nodes
|
|
#onode = find_onode(node_tree)
|
|
|
|
#Get all nodes with label "Viewer"
|
|
viewer_nodes = [n for n in nodes if n.label == "Viewer"]
|
|
|
|
#Check if any of those viewer nodes are connected to the Material Output
|
|
for n in viewer_nodes:
|
|
#if n.name == onode.inputs[0].links[0].from_node.name:
|
|
if len(n.outputs[0].links)>1:
|
|
if n.outputs[0].links[0].to_node.type == "OUTPUT_MATERIAL":
|
|
return True
|
|
|
|
return False
|
|
|
|
def check_mats_valid_for_pbr(context, mat):
|
|
prefs = context.preferences.addons[base_package].preferences
|
|
skip = prefs.skip_pbr_nodes_check
|
|
|
|
allowed_list = convertable_shaders + [
|
|
"ShaderNodeBsdfPrincipled", "ShaderNodeMixShader",
|
|
"ShaderNodeEmission", "NodeReroute", "ShaderNodeGroup",
|
|
"NodeGroupInput", "NodeGroupOutput"]
|
|
|
|
|
|
if skip:
|
|
print_message(context, "Skipping check for invalid PBR nodes")
|
|
return []
|
|
|
|
sbp = context.scene.SimpleBake_Props
|
|
node_sets = [mat.node_tree.nodes]
|
|
invalid_node_names = []
|
|
|
|
i = 0
|
|
while i < len(node_sets):
|
|
this_node_set = node_sets[i]
|
|
|
|
#Find any node groups, and add their node_set to the list
|
|
groups = [n.name for n in this_node_set if n.bl_idname == "ShaderNodeGroup"]
|
|
for n_name in groups:
|
|
if(n:=this_node_set.get(n_name)):
|
|
if hasattr(n, "node_tree"):
|
|
node_tree = n.node_tree
|
|
if hasattr(node_tree, "nodes"):
|
|
node_sets.append(n.node_tree.nodes)
|
|
|
|
#Now check for offending nodes in this node set
|
|
offending = [n for n in this_node_set if
|
|
len(n.outputs)>0 and
|
|
n.outputs[0].type == "SHADER" and
|
|
len(n.outputs[0].links) > 0 and
|
|
n.bl_idname not in allowed_list]
|
|
for o in offending:
|
|
invalid_node_names.append(o.name)
|
|
|
|
#Second case, the shader to RGB node
|
|
offending = ([n for n in this_node_set if
|
|
len(n.outputs)>0 and
|
|
len(n.outputs[0].links) > 0 and
|
|
n.bl_idname == "ShaderNodeShaderToRGB"
|
|
])
|
|
for o in offending:
|
|
invalid_node_names.append(o.name)
|
|
|
|
i+=1
|
|
|
|
return invalid_node_names
|
|
|
|
def ensure_valid_material_config(context, obj):
|
|
|
|
sbp = context.scene.SimpleBake_Props
|
|
|
|
if "SimpleBake_Placeholder" in bpy.data.materials:
|
|
mat = bpy.data.materials["SimpleBake_Placeholder"]
|
|
else:
|
|
mat = bpy.data.materials.new("SimpleBake_Placeholder")
|
|
bpy.data.materials["SimpleBake_Placeholder"].use_nodes = True
|
|
|
|
# Assign it to object
|
|
if len(obj.material_slots) > 0:
|
|
#Assign it to every empty slot
|
|
for slot in obj.material_slots:
|
|
if slot.material == None:
|
|
slot.material = mat
|
|
else:
|
|
# no slots
|
|
obj.data.materials.append(mat)
|
|
|
|
#All materials must use nodes
|
|
for slot in obj.material_slots:
|
|
mat = slot.material
|
|
if mat.use_nodes == False:
|
|
mat.use_nodes = True
|
|
|
|
return True
|
|
|
|
|
|
def ensure_target_object_bake_visibility(obj):
|
|
if obj == None:
|
|
return
|
|
|
|
obj.hide_viewport = False
|
|
obj.hide_set(False)
|
|
obj.hide_select = False
|
|
obj.hide_render = False
|
|
|
|
|
|
class SimpleBake_OT_Starting_Checks(Operator):
|
|
"""Perform pre-bake starting checks"""
|
|
bl_idname = "simplebake.starting_checks"
|
|
bl_description = "Perform bake starting checks"
|
|
bl_label = "Go"
|
|
|
|
def universal_checks(self, context):
|
|
sbp = context.scene.SimpleBake_Props
|
|
print_message(context, "Universal pre-bake checks")
|
|
bake_objects = self.bake_objects
|
|
targetobj = self.targetobj
|
|
targetobj_cycles = self.targetobj_cycles
|
|
messages = []
|
|
|
|
#Universal Checks
|
|
if(context.mode != "OBJECT"):
|
|
messages.append("ERROR: Not in object mode")
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#If exporting, check we can write to the folder given
|
|
if sbp.save_bakes_external or sbp.save_obj_external:
|
|
path = Path(bpy.path.abspath(sbp.export_path))
|
|
#path = path.parent
|
|
if not can_write_to_location(path):
|
|
messages.append(f"ERROR: Blender reports that it cannot write to the Export Path you have")
|
|
messages.append(f'specified under Export settings')
|
|
messages.append(f'CANNOT WRITE TO: "{bpy.path.abspath(str(path))}"')
|
|
messages.append(f"Try specifying a different export path")
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#Check no cp textures rely on bakes that are no longer enabled
|
|
if sbp.global_mode == SBConstants.PBR:
|
|
pbr_bakes = pbr_selections_to_list(context)
|
|
special_bakes = specials_selection_to_list(context)
|
|
bakes = pbr_bakes + special_bakes
|
|
bakes.append("none")
|
|
def cp_channel_invalid(value):
|
|
"""Return True if this channel value can't be satisfied by the current bake settings."""
|
|
if value == "none":
|
|
return False
|
|
if "||" in value:
|
|
# Per-material value — only valid when tex_per_mat is on
|
|
if not sbp.tex_per_mat:
|
|
return True
|
|
bake_type = value.split("||", 1)[0]
|
|
else:
|
|
bake_type = value
|
|
return bake_type not in bakes
|
|
def cp_display(value):
|
|
"""Human-readable form of a channel value, safe for the | -delimited message box."""
|
|
return value.replace("||", " - ")
|
|
for cpt in sbp.cp_list:
|
|
if cp_channel_invalid(cpt.R):
|
|
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.R)}, but you are no longer baking it")
|
|
if cp_channel_invalid(cpt.G):
|
|
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.G)}, but you are no longer baking it")
|
|
if cp_channel_invalid(cpt.B):
|
|
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.B)}, but you are no longer baking it")
|
|
if cp_channel_invalid(cpt.A):
|
|
messages.append(f"ERROR: Channel packed texture \"{cpt.name}\" depends on {cp_display(cpt.A)}, but you are no longer baking it")
|
|
messages = list(set(messages)) #Remove duplicates
|
|
if len(messages) >0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#Is anything seleccted at all for bake?
|
|
if len(bake_objects) == 0:
|
|
messages.append("ERROR: Nothing in the bake objects list!")
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#Check everything selected (or target) is mesh
|
|
l = [bpy.data.objects.get(name) for name in bake_objects]
|
|
l = [o for o in l if o!=None and o.type != "MESH"]
|
|
|
|
if len(l)>0:
|
|
([messages.append(f"ERROR: Object '{obj.name}' is not mesh") for obj in l])
|
|
if sbp.selected_s2a and targetobj != None and targetobj.type != "MESH":
|
|
messages.append(f"ERROR: Object '{targetobj.name}' (your target object) is not mesh")
|
|
if sbp.cycles_s2a and targetobj_cycles != None and targetobj_cycles.type != "MESH":
|
|
messages.append(f"ERROR: Object '{targetobj.name}' (your target object) is not mesh")
|
|
if len(messages) > 0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#if sbp.save_bakes_external or sbp.save_obj_external:
|
|
#if sbp.save_external_folder != clean_file_name(sbp.save_external_folder):
|
|
#message_items = ["ERROR: Folder name can only contain characters that",
|
|
#"are valid for the external file system"]
|
|
#show_message_box(context, message_items, "ERROR", icon = "ERROR")
|
|
#return {'CANCELLED'}
|
|
|
|
|
|
#Ensure valid material config
|
|
objs = [bpy.data.objects.get(name) for name in bake_objects]
|
|
objs = [o for o in objs if o!=None]
|
|
for obj in objs:
|
|
ensure_valid_material_config(context, obj)
|
|
if targetobj!=None: ensure_valid_material_config(context, targetobj)
|
|
if targetobj_cycles!=None: ensure_valid_material_config(context, targetobj_cycles)
|
|
|
|
#Check object visibility
|
|
if len([o for o in context.view_layer.objects if o.type=="MESH"])==0:
|
|
messages.append("There must be at least one mesh object visible in the current view layer")
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
if sbp.selected_s2a:
|
|
ensure_target_object_bake_visibility(sbp.targetobj)
|
|
if sbp.cycles_s2a:
|
|
ensure_target_object_bake_visibility(sbp.targetobj_cycles)
|
|
|
|
obj_test_list = bake_objects.copy()
|
|
if sbp.selected_s2a and sbp.targetobj != None:
|
|
obj_test_list.append(sbp.targetobj.name)
|
|
if sbp.cycles_s2a and sbp.targetobj_cycles != None:
|
|
obj_test_list.append(sbp.targetobj_cycles.name)
|
|
|
|
for obj_name in obj_test_list:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
if o.hide_viewport == True:
|
|
messages.append(f"Object '{o.name}' is hidden in viewport (monitor icon in outliner)")
|
|
if o.hide_render == True:
|
|
messages.append(f"Object '{o.name}' is hidden for render (camera icon in outliner)")
|
|
if o.hide_get() == True:
|
|
messages.append(f"Object '{o.name}' is hidden in viewport eye (eye icon in outliner)")
|
|
if o.hide_select == True:
|
|
messages.append(f"Object '{o.name}' is hidden for selection (arrow icon in outliner)")
|
|
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#None of the objects can have zero faces
|
|
for obj_name in bake_objects:
|
|
o = bpy.data.objects.get(obj_name)
|
|
if o!= None and len(o.data.polygons) < 1: messages.append(f"ERROR: Object '{o.name}' has no faces")
|
|
if sbp.selected_s2a and targetobj != None:
|
|
if len(targetobj.data.polygons) < 1: messages.append(f"ERROR: Object '{targetobj.name}' has no faces")
|
|
if sbp.cycles_s2a and targetobj_cycles != None:
|
|
if len(targetobj_cycles.data.polygons) < 1: messages.append(f"ERROR: Object '{targetobj_cycles.name}' has no faces")
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#Check for viewer nodes still connected
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
for slot in o.material_slots:
|
|
mat = slot.material
|
|
if mat != None: #It'll get a placeholder material later on if it's none
|
|
if check_for_connected_viewer_node(context, mat):
|
|
messages.append(f"ERROR: Material '{mat.name}' on object '{o.name}' has a Viewer node connected to the Material Output")
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#glTF
|
|
if sbp.create_glTF_node:
|
|
if sbp.glTF_selection == SBConstants.AO and not sbp.selected_ao:
|
|
messages.append(f"ERROR: You have selected AO for glTF settings (in the 'Other Settings' section), but you aren't baking AO")
|
|
if sbp.glTF_selection == SBConstants.LIGHTMAP and not sbp.selected_lightmap:
|
|
messages.append(f"ERROR: You have selected Lightmap for glTF settings (in the 'Other Settings' section), but you aren't baking Lightmap")
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
prefs = context.preferences.addons[__package__].preferences
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
if o.name != clean_file_name(o.name) and sbp.save_bakes_external and "%OBJ%" in prefs.img_name_format:
|
|
messages.append(f"ERROR: You are trying to save external images, but object with name \"{o.name}\" contains invalid characters for saving externally.")
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#Merged bake checks
|
|
if sbp.merged_bake and sbp.merged_bake_name == "":
|
|
messages.append(f"ERROR: You are baking multiple objects to one texture set, but the texture name is blank")
|
|
if (sbp.merged_bake_name != clean_file_name(sbp.merged_bake_name)) and sbp.save_bakes_external:
|
|
messages.append(f"ERROR: The texture name you inputted for baking multiple objects to one texture set (\"{sbp.merged_bake_name}\") contains invalid characters for saving externally.")
|
|
|
|
if sbp.merged_bake:
|
|
if sbp.selected_s2a or sbp.cycles_s2a:
|
|
if sbp.s2a_opmode!="automatch":
|
|
messages.append("You can't use the Bake Multiple Objects to One Texture Set option when baking to target")
|
|
|
|
#Export bakes checkes
|
|
if (sbp.save_bakes_external or sbp.save_obj_external) and sbp.export_path == "":
|
|
messages.append(f"You are saving your bakes and/or mesh externally, but your "
|
|
"Export Path (under the Export settings section) is blank")
|
|
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
# Background bake + UDIMs requires external save to be enabled
|
|
if sbp.bgbake == "bg" and not sbp.save_bakes_external:
|
|
if sbp.selected_s2a:
|
|
udim_check_objects = [targetobj.name] if targetobj else []
|
|
elif sbp.cycles_s2a:
|
|
udim_check_objects = [targetobj_cycles.name] if targetobj_cycles else []
|
|
else:
|
|
udim_check_objects = list(bake_objects)
|
|
|
|
for obj_name in udim_check_objects:
|
|
result = get_cached_udim_tiles(context, obj_name, threshold=0.001)
|
|
if result["total_tiles"] > 1:
|
|
messages.append("ERROR: Bake objects have UDIMs. Background baking with UDIMs")
|
|
messages.append('requires "Export Bakes" to be enabled in the Export settings.')
|
|
show_message_box(context, messages, "Errors occurred", icon='ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def pbr_no_s2a_checks(self, context):
|
|
sbp = context.scene.SimpleBake_Props
|
|
print_message(context, "PBR no S2A pre-bake checks")
|
|
bake_objects = self.bake_objects
|
|
targetobj = self.targetobj
|
|
targetobj_cycles = self.targetobj_cycles
|
|
messages = []
|
|
|
|
#Check if we have a shader node editor to use_nodes
|
|
ngs = False
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
for slot in o.material_slots:
|
|
if slot.material != None:
|
|
ngs = has_node_groups(context, slot.material.name)
|
|
break
|
|
if ngs:
|
|
break
|
|
if ngs and not find_shader_node_editor():
|
|
messages.append(f"Some or all of your materials have node groups. To process node groups, SimpleBake needs there")
|
|
messages.append(f"to be a Shader Node Editor open somewhere in one of your Blender workspaces. You don't seem to have")
|
|
messages.append(f"one open anywhere! Open a Shader Node Editor (as if you were editing a material) somewhere in one of")
|
|
messages.append(f"your Blender workspaces, then try again")
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#If baking displacement, do all materials meet the rules
|
|
if sbp.selected_displacement:
|
|
mats = {}
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
for slot in o.material_slots:
|
|
if slot.material != None:
|
|
mats[obj_name] = slot.material.name
|
|
for m in mats:
|
|
if not check_mat_valid_for_displacement(mats[m], m, messages):
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
for obj_name in bake_objects:
|
|
|
|
if not (o:=bpy.data.objects.get(obj_name)):
|
|
break
|
|
|
|
#Are UVs OK?
|
|
generating_uvs = sbp.new_uv_option or (sbp.tex_per_mat and sbp.expand_mat_uvs)
|
|
if not generating_uvs and len(o.data.uv_layers) == 0:
|
|
messages.append(f"ERROR: Object {o.name} has no UVs, and you aren't generating new ones")
|
|
|
|
if sbp.new_uv_option and sbp.new_uv_method=='CombineExisting' and len(o.data.uv_layers) == 0:
|
|
messages.append(f"ERROR: You are combining existing UVs, but object {o.name} has no UVs")
|
|
|
|
|
|
#Do all materials have valid PBR config?
|
|
for slot in o.material_slots:
|
|
mat = slot.material
|
|
result = check_mats_valid_for_pbr(context, mat)
|
|
warn = False
|
|
if len(result) > 0:
|
|
for node_name in result:
|
|
messages.append(f"ERROR: Node '{node_name}' in material '{mat.name}' on object '{o.name}' is not valid for PBR bake. Principled BSDFs and/or Emission only!")
|
|
warn = True
|
|
if warn:
|
|
messages.append("NOTE: There is an option to disable this check in the SimpleBake addon preferences, but it is not recommended. Baking PBR with nodes that are not PBR compatible may give unexpected results, Black bakes or even CRASHES!")
|
|
|
|
#Checked for invalid nodes, now let's check for just absent any useful nodes:
|
|
else:
|
|
|
|
#Do all materials have at least one material output node?
|
|
for slot in o.material_slots:
|
|
mat = slot.material
|
|
mos = [node for node in mat.node_tree.nodes if node.bl_idname == "ShaderNodeOutputMaterial"\
|
|
and len(node.inputs[0].links)>0]
|
|
if len(mos) == 0:
|
|
messages.append(f"ERROR: Material '{mat.name}' on object '{o.name}' is broken. Must have at least one Material Output node that's plugged in!")
|
|
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def pbr_s2a_checks(self, context):
|
|
sbp = context.scene.SimpleBake_Props
|
|
print_message(context, "PBR S2A pre-bake checks")
|
|
bake_objects = self.bake_objects
|
|
targetobj = self.targetobj
|
|
targetobj_cycles = self.targetobj_cycles
|
|
messages = []
|
|
|
|
ngs = False
|
|
for obj_name in bake_objects:
|
|
if(o:=bpy.data.objects.get(obj_name)):
|
|
for slot in o.material_slots:
|
|
if slot.material != None:
|
|
ngs = has_node_groups(context, slot.material.name)
|
|
break
|
|
if ngs:
|
|
break
|
|
if ngs and not find_shader_node_editor():
|
|
messages.append(f"Some or all of your materials have node groups. To process node groups, SimpleBake needs there")
|
|
messages.append(f"to be a Shader Node Editor open somewhere in one of your Blender workspaces. You don't seem to have")
|
|
messages.append(f"one open anywhere! Open a Shader Node Editor (as if you were editing a material) somewhere in one of")
|
|
messages.append(f"your Blender workspaces, then try again")
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
|
|
#Do all materials have valid PBR config?
|
|
warn = False
|
|
for obj_name in bake_objects:
|
|
if(o:=bpy.data.objects.get(obj_name)):
|
|
for slot in o.material_slots:
|
|
mat = slot.material
|
|
result = check_mats_valid_for_pbr(context, mat)
|
|
if len(result) > 0:
|
|
for node_name in result:
|
|
messages.append(f"ERROR: Node '{node_name}' in material '{mat.name}' on object '{o.name}' is not valid for PBR bake. Principled BSDFs and/or Emission only!")
|
|
warn = True
|
|
if warn:
|
|
messages.append("NOTE: There is an option to disable this check in the SimpleBake addon preferences, but it is not recommended. Baking PBR with nodes that are not PBR compatible may give unexpected results, Black bakes or even CRASHES!")
|
|
|
|
#Checked for invalid nodes, now let's check for just absent any useful nodes:
|
|
else:
|
|
#Do all materials have at least one material output node?
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
for slot in o.material_slots:
|
|
mat = slot.material
|
|
mos = [node for node in mat.node_tree.nodes if node.bl_idname == "ShaderNodeOutputMaterial"\
|
|
and len(node.inputs[0].links)>0]
|
|
if len(mos) == 0:
|
|
messages.append(f"ERROR: Material '{mat.name}' on object '{o.name}' is broken. Must have at least one Material Output node that's plugged in!")
|
|
|
|
#Checks for baking displacement
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
for slot in o.material_slots:
|
|
if slot.material!=None:
|
|
mat = slot.material
|
|
if not check_mat_valid_for_displacement(mat.name, o.name, messages):
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", icon = 'ERROR')
|
|
return {'CANCELLED'}
|
|
|
|
#From this point onward, we only care about the target object
|
|
#-------------------------------------------------------------------------
|
|
|
|
#Do we have a target object?
|
|
if targetobj == None:
|
|
messages.append("ERROR: You are trying to bake to a target object with PBR Bake, but you have not selected one in the SimpleBake panel")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
#If declas, need to do the displacement check again as the target object materials get baked
|
|
for slot in targetobj.material_slots:
|
|
if slot.material!=None:
|
|
mat = slot.material
|
|
if not check_mat_valid_for_displacement(mat.name, targetobj.name, messages):
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
|
|
#Have we got more selected than just the target object?
|
|
if len(bake_objects) == 1 and bake_objects[0] == targetobj.name:
|
|
messages.append("ERROR: You are trying to bake to a target object, but the only object you have selected is your target")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
#If the target object is in the bake objects list, remove it now
|
|
i = sbp.objects_list.find(targetobj.name)
|
|
if i>-1: sbp.objects_list.remove(i)
|
|
|
|
#Are UVs OK?
|
|
if sbp.new_uv_option == False and len(targetobj.data.uv_layers) == 0:
|
|
messages.append(f"ERROR: Object {targetobj.name} has no UVs, and you aren't generating new ones")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
#All existing materials must use nodes
|
|
for slot in targetobj.material_slots:
|
|
if slot.material != None:
|
|
if not slot.material.use_nodes:
|
|
slot.material.use_nodes = True
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
def cyclesbake_no_s2a_checks(self, context):
|
|
sbp = context.scene.SimpleBake_Props
|
|
print_message(context, "CyclesBake no S2A pre-bake checks")
|
|
bake_objects = self.bake_objects
|
|
targetobj = self.targetobj
|
|
targetobj_cycles = self.targetobj_cycles
|
|
messages = []
|
|
|
|
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
#Are UVs OK?
|
|
if not sbp.tex_per_mat:
|
|
if sbp.new_uv_option == False and len(o.data.uv_layers) == 0:
|
|
messages.append(f"ERROR: Object {o.name} has no UVs, and you aren't generating new ones")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
if sbp.new_uv_option and sbp.new_uv_method=='CombineExisting' and len(o.data.uv_layers) == 0:
|
|
messages.append(f"ERROR: You are combining existing UVs, but object {o.name} has no UVs")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
else:
|
|
if sbp.expand_mat_uvs == False and len(o.data.uv_layers) == 0:
|
|
messages.append(f"ERROR: Object {o.name} has no UVs, and you aren't generating new ones")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def cyclesbake_s2a_checks(self, context):
|
|
sbp = context.scene.SimpleBake_Props
|
|
print_message(context, "CyclesBake S2A pre-bake checks")
|
|
bake_objects = self.bake_objects
|
|
targetobj = self.targetobj
|
|
targetobj_cycles = self.targetobj_cycles
|
|
messages = []
|
|
|
|
#Do we actually have a target object?
|
|
if targetobj_cycles == None:
|
|
messages.append(f"ERROR: You are trying to bake to target with CyclesBake, but there is no target object selected on the SimpleBake panel")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
#Have we got more selected than just the target object?
|
|
elif len(bake_objects) == 1 and bake_objects[0] == targetobj_cycles.name:
|
|
messages.append("ERROR: You are trying to bake selected to active with CyclesBake, but the only object you have selected is your active (target) object")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
#If the target object is in the bake objects list, remove it now
|
|
i = sbp.objects_list.find(targetobj_cycles.name)
|
|
if i>-1: sbp.objects_list.remove(i)
|
|
|
|
#Are UVs OK?
|
|
elif not sbp.new_uv_option and len(targetobj_cycles.data.uv_layers) == 0:
|
|
messages.append(f"ERROR: Object {targetobj_cycles.name} has no UVs, and you aren't generating new ones")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
|
|
return {'FINISHED'}
|
|
|
|
def specials_checks(self, context):
|
|
sbp = context.scene.SimpleBake_Props
|
|
print_message(context, "Specials pre-bake checks")
|
|
bake_objects = self.bake_objects
|
|
targetobj = self.targetobj
|
|
targetobj_cycles = self.targetobj_cycles
|
|
messages = []
|
|
|
|
if sbp.selected_col_vertex:
|
|
|
|
if not sbp.selected_s2a and not sbp.cycles_s2a:
|
|
for obj_name in bake_objects:
|
|
if (o:=bpy.data.objects.get(obj_name)):
|
|
if len(o.data.color_attributes) == 0:
|
|
messages.append(f"You are trying to bake the active vertex colours, but object {o.name} doesn't have vertex colours")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
if sbp.cycles_s2a or sbp.selected_s2a:
|
|
if sbp.selected_s2a: target_obj = sbp.targetobj
|
|
else: target_obj = sbp.targetobj_cycles
|
|
target_obj_name = target_obj.name
|
|
|
|
obj_names = [o.obj_point.name for o in sbp.objects_list]
|
|
if target_obj_name in obj_names:
|
|
obj_names.remove(target_obj_name)
|
|
|
|
for o_name in obj_names:
|
|
if(o:=context.scene.objects.get(o_name)):
|
|
if len(o.data.color_attributes) == 0:
|
|
messages.append(f"You are trying to bake the active vertex colours, but object {o.name} doesn't have vertex colours")
|
|
if len(messages)>0:
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
def image_sequence_checks(self,context):
|
|
sbp = context.scene.SimpleBake_Props
|
|
print_message(context, "Image sequence pre-bake checks")
|
|
messages = []
|
|
if not sbp.image_sequence_start_frame < sbp.image_sequence_end_frame:
|
|
messages.append("When baking an image sequence, the starting frame must be before the end frame. Change this in the Other Settings section of SimpleBake")
|
|
show_message_box(context, messages, "Errors occured", "ERROR")
|
|
return {'CANCELLED'}
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
def execute(self, context):
|
|
blender_refresh()
|
|
sbp = context.scene.SimpleBake_Props
|
|
refresh_bake_objects_list(context)
|
|
#disable_impossible(context)
|
|
|
|
#sbp.bake_done = False
|
|
|
|
#Get objects
|
|
self.bake_objects = [i.name for i in sbp.objects_list]
|
|
self.targetobj = sbp.targetobj
|
|
self.targetobj_cycles = sbp.targetobj_cycles
|
|
|
|
print_message(context, "Starting checks operator")
|
|
|
|
|
|
|
|
#Universal checks
|
|
r = self.universal_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
#PBR Bake Checks - No S2A
|
|
if sbp.global_mode == SBConstants.PBR and not sbp.selected_s2a:
|
|
r = self.pbr_no_s2a_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
#PBR Bake - S2A
|
|
if sbp.global_mode == SBConstants.PBR and sbp.selected_s2a:
|
|
r = self.pbr_s2a_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
#If decals, need to do a normal PBR check on the target object
|
|
if sbp.s2a_opmode == "decals":
|
|
self.bake_objects= [sbp.targetobj.name]
|
|
r = self.pbr_no_s2a_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
|
|
#CyclesBake - no S2A
|
|
if sbp.global_mode == SBConstants.CYCLESBAKE and not sbp.cycles_s2a:
|
|
r = self.cyclesbake_no_s2a_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
#CyclesBake - S2A
|
|
if sbp.global_mode == SBConstants.CYCLESBAKE and sbp.cycles_s2a:
|
|
r = self.cyclesbake_s2a_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
#Specials
|
|
if len(specials_selection_to_list(context)) >0:
|
|
r = self.specials_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
#Image sequence
|
|
if sbp.image_sequence_enabled:
|
|
r = self.image_sequence_checks(context)
|
|
if 'CANCELLED' in r: return{'CANCELLED'}
|
|
|
|
|
|
#Fix that weird context incorrect error
|
|
#Selected object at start must be something and must be mesh
|
|
#Find a nominal object that is a mesh and in the current view layer
|
|
if context.active_object == None or context.active_object.type != "MESH":
|
|
nominal = None
|
|
for o in context.view_layer.objects:
|
|
if o.type == "MESH":
|
|
nominal = o
|
|
break
|
|
assert(nominal!=None)
|
|
|
|
bpy.ops.simplebake.select_only_this(object_name=nominal.name)
|
|
|
|
|
|
#Guess everything was OK
|
|
return {'FINISHED'}
|
|
|
|
|
|
"""
|
|
#Specials Bake
|
|
#CAGE OBJECT BROKEN? CHECK IF NOT NONE AND, IF NOT, FLIP THE SWITdisable_impossibleCH TO USE CAGE
|
|
|
|
"""
|
|
|
|
|
|
classes = ([
|
|
SimpleBake_OT_Starting_Checks
|
|
])
|
|
|
|
def register():
|
|
|
|
global classes
|
|
for cls in classes:
|
|
register_class(cls)
|
|
|
|
def unregister():
|
|
global classes
|
|
for cls in classes:
|
|
unregister_class(cls)
|