framework/cg/blender/scripts/addons/BakeWrangler/baker.py
2023-10-25 10:54:36 +00:00

3030 lines
144 KiB
Python

# Information needed to create a desired output. A solution has a destination file
# name and output format along with a list of the bake passes that are needed and
# how they are combined into the final image
class bake_solution():
def __init__(self, socket, alpha, vcol):
self.bakepass = {} # Dict of Pass Nodes to process
self.baketile = {} # Dict keyed by Pass Nodes with a dict of UDIM tiles when enabled
self.bakecols = {} # Dict keyed by Object with Pass node and name of color data
self.postproc = None# Material containing post processing information
self.dopost = False # Enabled if post process nodes are set up
self.sock = socket # Socket of the output node for which this is the solution
self.alpha = alpha # Socket with alpha channel input
self.vcol = vcol # Is a vertex color output
self.format = socket.node.get_settings() # Dict of format settings
self.split = socket.node.get_split_objects() # List of objects to split output for
self.get_passes(socket, False, vcol)
if not vcol and alpha.enabled:
self.get_passes(alpha, True, vcol)
# Clear all results from solution
def clear_results(self):
for key in self.bakepass.keys():
node = self.bakepass[key]
node.bake_result = None
node.mask_result = None
node.sbake_result = None
node.smask_result = None
node.vcol_result = None
for key in self.baketile.keys():
self.baketile[key] = {}
# Moves backwards down the tree from a given input socket and will be called
# recursively until all paths have ended in a bake pass
def get_passes(self, socket, is_alpha, is_vcol):
node = get_input(socket)
if not node: return
# Add the process, even if it's a pass (add func will sort it out)
self.add_postproc(node, socket, is_alpha, is_vcol)
if node.bl_idname == 'BakeWrangler_Bake_Pass':
# Add to pass list
self.bakepass[node.name] = node
self.baketile[node.name] = {}
# Initialize results
node.bake_result = None
node.mask_result = None
node.sbake_result = None
node.smask_result = None
node.vcol_result = None
else:
# It's not a pass node, so it must be a post process, recurse
self.dopost = True
for sock in node.inputs:
self.get_passes(sock, is_alpha, is_vcol)
# Adds the configuration from a post processing node into the post proc material
def add_postproc(self, post, from_sock, is_alpha, is_vcol):
#if self.postproc == None and (post.bl_idname == 'BakeWrangler_Bake_Pass' and not post.use_mask):
# return
if self.postproc == None:
if is_vcol:
self.postproc = post_proc_col.copy()
self.postproc.name = "BW_Post_Col" + self.sock.node.name + self.sock.suffix
else:
self.postproc = post_proc_mat.copy()
self.postproc.name = "BW_Post_" + self.sock.node.name + self.sock.suffix
nodes = self.postproc.node_tree.nodes
links = self.postproc.node_tree.links
# Reached bake pass node
if post.bl_idname == 'BakeWrangler_Bake_Pass':
if is_vcol:
# Add vertex color attribute node
if post.name not in nodes.keys():
vcols = nodes.new('ShaderNodeVertexColor')
vcols.name = post.name
# Originating node name stored in the label string so it can be looked up later
vcols.label = post.name
else:
# It's already been set up, but may need another link added
vcols = nodes[post.name]
self.link_postproc(from_sock, vcols.outputs['Color'])
return
self.link_postproc(from_sock, vcols.outputs['Color'])
else:
# Add a masked bake node set up
if post.name not in nodes.keys():
masked_bake = nodes.new('ShaderNodeGroup')
masked_bake.node_tree = post_masked_bake
masked_bake.name = post.name
# Originating node name stored in the label string so it can be looked up later
masked_bake.label = post.name
# Invert output for smoothness passes
if post.bake_picked in ['SMOOTHNESS']:
masked_bake.inputs['Invert'].default_value = 1.0
else:
masked_bake.inputs['Invert'].default_value = 0.0
else:
# It's already been set up, but may need another link added
masked_bake = nodes[post.name]
self.link_postproc(from_sock, masked_bake.outputs['Bake'])
return
# Add and link image inputs
bake = nodes.new('ShaderNodeTexImage')
links.new(bake.outputs['Color'], masked_bake.inputs['Bake'])
mask = nodes.new('ShaderNodeTexImage')
links.new(mask.outputs['Color'], masked_bake.inputs['Mask'])
self.link_postproc(from_sock, masked_bake.outputs['Bake'])
# Channel mapping node
elif post.bl_idname == 'BakeWrangler_Channel_Map':
# Add channel map node
if post.name not in nodes.keys():
chan_map = nodes.new('ShaderNodeGroup')
chan_map.node_tree = post_chan_map.copy()
chan_map.name = post.name
else:
# It's already set up but may need another link added
chan_map = nodes[post.name]
self.link_postproc(from_sock, chan_map.outputs['Color'])
return
# Configure node internals (initial configuration has Color input set up)
chan_nodes = chan_map.node_tree.nodes
chan_links = chan_map.node_tree.links
for chan in post.inputs.keys():
if chan in ['Red', 'Green', 'Blue']:
if get_input(post.inputs[chan]):
# Connect up correct input
in_chan = post.inputs[chan].input_channel
if in_chan in ['Red', 'Green', 'Blue']:
chan_links.new(chan_nodes['Input'].outputs[chan], chan_nodes[chan + '_From'].inputs[0])
chan_links.new(chan_nodes[chan + '_From'].outputs[in_chan], chan_nodes['RGB_Map'].inputs[chan])
else:
chan_links.new(chan_nodes['Input'].outputs[chan], chan_nodes[chan + '_From_V'].inputs[0])
chan_links.new(chan_nodes[chan + '_From_V'].outputs[0], chan_nodes['RGB_Map'].inputs[chan])
# Link to prev
self.link_postproc(from_sock, chan_map.outputs['Color'])
# Mix RGB
elif post.bl_idname == 'BakeWrangler_Post_MixRGB':
# Add mix node
if post.name not in nodes.keys():
mix_rgb = nodes.new('ShaderNodeMixRGB')
mix_rgb.name = post.name
mix_rgb.blend_type = post.op
mix_rgb.inputs['Fac'].default_value = post.inputs['Fac'].value_fac
mix_rgb.inputs['Color1'].default_value = post.inputs['Color1'].value_rgb
mix_rgb.inputs['Color2'].default_value = post.inputs['Color2'].value_rgb
# Link input
mix_rgb = nodes[post.name]
self.link_postproc(from_sock, mix_rgb.outputs['Color'])
# Split RGB
elif post.bl_idname == 'BakeWrangler_Post_SplitRGB':
# Add split node
if post.name not in nodes.keys():
split_rgb = nodes.new('ShaderNodeSeparateColor')
split_rgb.name = post.name
# Link input
split_rgb = nodes[post.name]
self.link_postproc(from_sock, split_rgb.outputs[from_sock.links[0].from_socket.name])
elif post.bl_idname == 'BakeWrangler_Post_JoinRGB':
# Add join node
if post.name not in nodes.keys():
join_rgb = nodes.new('ShaderNodeCombineColor')
join_rgb.name = post.name
for chan in join_rgb.inputs:
chan.default_value = post.inputs[chan.name].value_col
# Link input
join_rgb = nodes[post.name]
self.link_postproc(from_sock, join_rgb.outputs['Color'])
elif post.bl_idname == 'BakeWrangler_Post_Math':
# Add maths node
if post.name not in nodes.keys():
maths = nodes.new('ShaderNodeMath')
maths.name = post.name
maths.operation = post.op
for val in post.inputs:
maths.inputs[int(val.identifier)].default_value = val.value
# Link input
maths = nodes[post.name]
self.link_postproc(from_sock, maths.outputs['Value'])
elif post.bl_idname == 'BakeWrangler_Post_Gamma':
# Add gamma node
if post.name not in nodes.keys():
gamma = nodes.new('ShaderNodeGamma')
gamma.name = post.name
gamma.inputs['Gamma'].default_value = post.inputs['Gamma'].value_gam
# Link input
gamma = nodes[post.name]
self.link_postproc(from_sock, gamma.outputs['Color'])
# Link post proc to previous entry
def link_postproc(self, from_sock, to_sock):
nodes = self.postproc.node_tree.nodes
links = self.postproc.node_tree.links
if from_sock.node.bl_idname == 'BakeWrangler_Output_Vertex_Cols':
prev_sock = nodes['bw_emit'].inputs['Color']
elif from_sock.node.bl_idname == 'BakeWrangler_Output_Image_Path':
if from_sock.name == 'Color':
prev_sock = nodes['AlphaSwitch'].inputs['Out']
else:
prev_sock = nodes['AlphaMap'].inputs['Alpha']
# Set up value mapping (default uses Value)
if from_sock.input_channel in ['R', 'G', 'B']:
nodes['AlphaMap'].node_tree = nodes['AlphaMap'].node_tree.copy()
amap_nodes = nodes['AlphaMap'].node_tree.nodes
amap_links = nodes['AlphaMap'].node_tree.links
amap_links.new(amap_nodes['Alpha_Map'].outputs[from_sock.input_channel], amap_nodes['Output'].inputs['Alpha'])
elif from_sock.node.bl_idname == 'BakeWrangler_Post_Math':
# Determine which input it is since they have the same name
prev_sock = nodes[from_sock.node.name].inputs[int(from_sock.identifier)]
else:
prev_sock = nodes[from_sock.node.name].inputs[from_sock.name]
links.new(to_sock, prev_sock)
# Process the node tree with the given node as the starting point
def process_tree(tree_name, node_name, socket):
# Create a base scene to work from that has every object in it
global base_scene
global mesh_scene
global active_scene
global current_frame
global frame_range
global padding
global anim_seed
base_scene = bpy.data.scenes.new("BakeWrangler_Base")
mesh_scene = bpy.data.scenes.new("BakeWrangler_Mesh")
active_scene = bpy.context.window.scene
current_frame = active_scene.frame_current
frame_range = None
padding = None
anim_seed = False
bpy.context.window.scene = base_scene
# For now use active scenes current animation frame (maybe more advanced options later)
base_scene.frame_current = current_frame
for obj in bpy.data.objects:
base_scene.collection.objects.link(obj)
# Add a property on objects that can link to a copy made
bpy.types.Object.bw_copy = bpy.props.PointerProperty(name="Object Copy", description="Copy with modifiers applied", type=bpy.types.Object)
bpy.types.Object.bw_copy_frame = bpy.props.IntProperty(name="Object Copy frame", description="Frame number when created", default=current_frame)
bpy.types.Object.bw_strip = bpy.props.PointerProperty(name="Object Stripped", description="Copy with modifiers stripped", type=bpy.types.Object)
bpy.types.Object.bw_strip_frame = bpy.props.IntProperty(name="Object Stripped frame", description="Frame number when created", default=current_frame)
bpy.types.Object.bw_vcols = bpy.props.PointerProperty(name="Object VCol Copy", description="Copy with modifiers intact", type=bpy.types.Object)
bpy.types.Object.bw_auto_cage = bpy.props.PointerProperty(name="Cage", description="Bake Wrangler auto generated cage", type=bpy.types.Object)
# Get tree position
tree = bpy.data.node_groups[tree_name]
node = tree.nodes[node_name]
err = 0
solutions = None
global user_prop
user_prop = None
user_data = None
solution_itr = 0
frames_itr = 0
batch_itr = 0
global solution_restart
global frames_restart
global batch_restart
if debug: _print("> Debugging output enabled", tag=True)
modify_recipe(tree)
# Each output node will generate a dict of bake solutions which will then be processed. Results
# are isolated within an output and may be regenerated if used in multiple outputs.
if node.bl_idname in ['BakeWrangler_Output_Image_Path', 'BakeWrangler_Output_Vertex_Cols']:
# A single output node, generate its solution list and then process them
solutions = process_output_node(node, socket)
if node.bl_idname == 'BakeWrangler_Output_Vertex_Cols':
_print("> Processing [%s]: Creating %i vertex colors" % (node.get_name(), len(solutions.keys())), tag=True)
_print(">", tag=True)
for key in solutions.keys():
solution_itr += 1
if (solution_itr - 1) < solution_restart: continue
err += process_vcol_solution(solutions[key])
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
else:
# Check for frame ranges
frames = sorted(node.frame_range())
padding = node.frame_range(padding=True)
anim_seed = node.frame_range(animated=True)
if len(frames) <= 1:
if len(frames) == 1: current_frame = frames.pop()
_print("> Processing [%s]: Creating %i images" % (node.get_name(), len(solutions.keys())), tag=True)
_print(">", tag=True)
for key in solutions.keys():
solution_itr += 1
if (solution_itr - 1) < solution_restart: continue
err += process_solution(solutions[key])
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
elif len(frames) > 1:
pack_data()
frame_range = frames
if debug: _print("> Frame range: %s" % (frame_range), tag=True)
for frame in frames:
frames_itr += 1
if (frames_itr - 1) < frames_restart: continue
current_frame = frame
base_scene.frame_current = current_frame
_print("> Processing [%s][Frame %s]: Creating %i images" % (node.get_name(), current_frame, len(solutions.keys())), tag=True)
_print(">", tag=True)
for key in solutions.keys():
solution_itr += 1
if (solution_itr - 1) < solution_restart: continue
else: solution_restart = 0
err += process_solution(solutions[key])
solutions[key].clear_results()
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
_print("<PFRAM>%i</PFRAM>" % (frames_itr))
solution_itr = 0
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
free_data()
elif node.bl_idname == 'BakeWrangler_Output_Batch_Bake':
_print("> Batch Mode [%i jobs]" % (len(node.inputs) - 1), tag=True)
# Batch mode has an optional user property that can be incremented, set that up
if node.user_prop_objt and node.user_prop_name:
user_data = node.user_prop_objt
user_prop = node.user_prop_name
if node.user_prop_zero:
user_data[user_prop] = 0
user_data.update_tag()
# Then similar to above, but update and increment the property between outputs
for batch_input in node.inputs:
batch = get_input(batch_input)
if batch:
batch_itr += 1
if (batch_itr - 1) < batch_restart: continue
solutions = process_output_node(batch)
if batch.bl_idname == 'BakeWrangler_Output_Vertex_Cols':
_print("> Processing [%s]: Creating %i vertex colors" % (batch.get_name(), len(solutions.keys())), tag=True)
_print(">", tag=True)
for key in solutions.keys():
solution_itr += 1
if (solution_itr - 1) < solution_restart: continue
else: solution_restart = 0
err += process_vcol_solution(solutions[key])
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
solution_itr = 0
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
else:
# Check for frame ranges
frames = batch.frame_range()
if len(frames) <= 1:
if len(frames) == 1: current_frame = frames[0]
_print("> Processing [%s]: Creating %i images" % (batch.get_name(), len(solutions.keys())), tag=True)
_print(">", tag=True)
for key in solutions.keys():
solution_itr += 1
if (solution_itr - 1) < solution_restart: continue
else: solution_restart = 0
err += process_solution(solutions[key])
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
solution_itr = 0
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
elif len(frames) > 1:
frame_range = frames
for frame in frames:
frames_itr += 1
if (frames_itr - 1) < frames_restart: continue
else: frames_restart = 0
current_frame = frame
_print("> Processing [%s][Frame %s]: Creating %i images" % (batch.get_name(), current_frame, len(solutions.keys())), tag=True)
_print(">", tag=True)
for key in solutions.keys():
solution_itr += 1
if (solution_itr - 1) < solution_restart: continue
else: solution_restart = 0
err += process_solution(solutions[key])
solutions[key].clear_results()
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
_print("<PFRAM>%i</PFRAM>" % (frames_itr))
solution_itr = 0
_print("<PSOLU>%i</PSOLU>" % (solution_itr))
free_data()
frames_itr = 0
_print("<PFRAM>%i</PFRAM>" % (frames_itr))
if user_prop != None:
# Increment user property
user_data[user_prop] += 1
user_data.update_tag()
_print("<PBATC%i</PBATC>" % (batch_itr))
else:
_print("> Invalid bake tree starting node", tag=True)
return True
return err
# To simplify doing the texture normal pass, the recipe will be modified if that pass is in use (maybe other stuff in future)
def modify_recipe(tree):
nodes = tree.nodes
links = tree.links
for node in nodes:
if node.bl_idname == 'BakeWrangler_Bake_Pass' and node.bake_picked == 'TEXNORM' and node.norm_space == 'TANGENT' and node.use_subtraction:
# Replace pass with a normal pass being subtracted from a object normals pass
norms = node
norms.bake_cat = 'CORE'
norms.bake_core = 'NORMAL'
objnorms = nodes.new('BakeWrangler_Bake_Pass')
objnorms.bake_cat = 'PBR'
objnorms.bake_pbr = 'OBJNORM'
# Connect all the same inputs to the objnorm node
for input in norms.inputs:
if input.islinked():
if input.name == 'Settings':
links.new(input.links[0].from_socket, objnorms.inputs['Settings'])
else:
links.new(input.links[0].from_socket, objnorms.inputs[-1])
# Create subtraction set up and insert it just after the pass nodes
in_sub = nodes.new('BakeWrangler_Post_MixRGB')
in_sub.op = 'SUBTRACT'
in_sub.inputs['Fac'].value_fac = 1.0
out_sub = nodes.new('BakeWrangler_Post_MixRGB')
out_sub.op = 'ADD'
out_sub.inputs['Fac'].value_fac = 1.0
out_sub.inputs['Color2'].value_rgb = [0.5, 0.5, 1.0, 1.0]
links.new(in_sub.outputs['Color'], out_sub.inputs['Color1'])
for link in norms.outputs['Color'].links:
links.new(out_sub.outputs['Color'], link.to_socket)
links.new(norms.outputs['Color'], in_sub.inputs['Color1'])
links.new(objnorms.outputs['Color'], in_sub.inputs['Color2'])
in_sub.inputs['Color2'].valid = True
# Process an output node into a list of bake solutions
def process_output_node(node, socket=-1):
solutions = {}
idx = 0
for sock in node.inputs:
if sock.bl_idname == 'BakeWrangler_Socket_Color' and get_input(sock):
if socket > -1:
if socket == idx:
solutions[sock] = bake_solution(sock, node.inputs[idx+1], node.bl_idname == 'BakeWrangler_Output_Vertex_Cols')
else:
solutions[sock] = bake_solution(sock, node.inputs[idx+1], node.bl_idname == 'BakeWrangler_Output_Vertex_Cols')
idx += 1
return solutions
# Process solution for vetex color output, passes will be baked and saved to temp files for transfer
def process_vcol_solution(solution):
err = 0
# The first step in any solution is to bake all the required passes that haven't been done yet
for key in solution.bakepass.keys():
if not solution.bakepass[key].vcol_result:
_print("> Pass: [%s] " % (solution.bakepass[key].get_name()), tag=True, wrap=False)
err += process_bake_pass(solution, key, solution.split)
_print(">", tag=True)
_print("> -Exporting vertex colors:", tag=True, wrap=False)
# Now process each objects output
for obj in solution.bakecols.keys():
if solution.dopost:
# Set input vertex colors
for node in solution.postproc.node_tree.nodes:
if node.bl_idname == 'ShaderNodeVertexColor':
# Check there is a vcol for this node
if node.label in solution.bakecols[obj].keys():
node.layer_name = solution.bakecols[obj][node.label]
berr, post_obj, post_vcol = bake_post_vcols(bpy.data.objects[obj].bw_vcols, solution.postproc, solution)
err += berr
if not berr:
verr, file = ipc.bake_verts(verts=post_obj.data.color_attributes[post_vcol], object=obj, name=solution.sock.suffix, type=solution.format['vcol_type'], domain=solution.format['vcol_domain'])
err += verr
if not verr and file:
pickled_verts.append(file)
else:
for bake in solution.bakecols[obj].keys():
verr, file = ipc.bake_verts(verts=bpy.data.objects[obj].bw_vcols.data.color_attributes[solution.bakecols[obj][bake]], object=obj, name=solution.sock.suffix, type=solution.format['vcol_type'], domain=solution.format['vcol_domain'])
err += verr
if not verr and file:
pickled_verts.append(file)
return err
# Process a solution, all passes need to be baked and any post processes done before saving to an image
def process_solution(solution):
err = 0
# The first step in any solution is to bake all the required passes that haven't been done yet
for key in solution.bakepass.keys():
if not solution.bakepass[key].bake_result:
_print("> Pass: [%s] " % (solution.bakepass[key].get_name()), tag=True, wrap=False)
err += process_bake_pass(solution, key, solution.split)
ret = 0
udim = solution.format['img_udim']
if solution.split:
# Go through each pass and generate just the objects in the split list saving each one
for obj in solution.split:
for key in solution.bakepass.keys():
bake = solution.bakepass[key].bake_result
if bake:
bake = bake.copy()
mask = solution.bakepass[key].mask_result
if mask:
mask = mask.copy()
ret = process_bake_pass(solution, key, [obj], False, [bake, mask])
if ret != -1:
err += ret
_print(">", tag=True)
if udim: # Output each udim
udims = []
for key in solution.baketile.keys():
for tile in solution.baketile[key].keys():
if len(solution.baketile[key][tile]) > 2 and tile not in udims:
udims.append(tile)
udims.sort()
if not err:
obname = obj[0].name
if len(obj) > 3: obname = obj[3]
if udim:
for tile in udims:
err += process_output(solution, obname, udim=tile)
else:
err += process_output(solution, obname)
if not solution.split:# or ret == -1:
_print(">", tag=True)
if udim: # Output each udim
udims = []
for key in solution.baketile.keys():
for tile in solution.baketile[key].keys():
if len(solution.baketile[key][tile]) and tile not in udims:
udims.append(tile)
udims.sort()
for tile in udims:
err += process_output(solution, udim=tile)
else:
err += process_output(solution)
return err
# Do post processing and generate output image
def process_output(solution, split="", udim=None):
err = 0
# Check for frame range
frame_str = ''
if frame_range or padding:
if padding:
pad_width = padding
else:
pad_width = len(str(max(frame_range)))
frame_str = '.{frame:0{width}}'.format(frame=current_frame, width=pad_width)
# Get file path
output_path = solution.sock.node.img_path
if udim is not None:
output_name = solution.sock.node.name_with_ext(split + solution.sock.suffix + "." + str(udim) + frame_str)
else:
output_name = solution.sock.node.name_with_ext(split + solution.sock.suffix + frame_str)
output_file = os.path.join(os.path.realpath(output_path), output_name)
# See if the output exists or if a new file should be created
orig_exists = False
if os.path.exists(output_file):
orig_image = bpy.data.images.load(os.path.abspath(output_file))
bw_solution_data['images'].append(orig_image)
orig_image.alpha_mode = 'CHANNEL_PACKED' # Prevent alpha changing output color
orig_exists = True
# Next post processing should take place as well as alpha channel creation if needed
_print("> Image: [%s] " % (output_name), tag=True)
post_alpha = None
if solution.format['img_color_mode'] == 'RGBA':
post_alpha = bpy.data.images.new(output_name + "alpha", solution.format['img_xres'], solution.format['img_yres'])
bw_solution_data['images'].append(post_alpha)
post_alpha.use_generated_float = solution.format['img_use_float']
post_alpha.alpha_mode = 'NONE'
post_alpha.colorspace_settings.name = solution.format['img_non_color']
post_alpha.colorspace_settings.is_data = True
# See if this solution will use marginer and need the combined mask
post_mask = None
if solution.format['marginer']:
post_mask = bpy.data.images.new(output_name + "mask", solution.format['img_xres'], solution.format['img_yres'])
bw_solution_data['images'].append(post_mask)
post_mask.alpha_mode = 'NONE'
post_mask.colorspace_settings.name = solution.format['img_non_color']
post_mask.colorspace_settings.is_data = True
# Create post color image
post_color = bpy.data.images.new(output_name + "color", solution.format['img_xres'], solution.format['img_yres'])
bw_solution_data['images'].append(post_color)
post_color.colorspace_settings.name = solution.format['img_color_space']
post_color.use_generated_float = solution.format['img_use_float']
nodes = solution.postproc.node_tree.nodes
links = solution.postproc.node_tree.links
# Set original image if it was loaded unless clear image is set
if orig_exists and not solution.format['img_clear']:
nodes['bw_input_orig'].image = orig_image
else:
nodes['bw_input_orig'].image = bpy.data.images["bw_default_orig"]
# Images in the post proc need to be set before running it
masks = []
for node in nodes:
if node.bl_idname == 'ShaderNodeGroup' and node.node_tree == post_masked_bake:
# Set the bake input
bake_img = None
bake_set = solution.bakepass[node.label].get_settings()
if node.inputs['Bake'].is_linked:
bake_img = node.inputs['Bake'].links[0].from_node
if bake_set['interpolate']:
bake_img.interpolation = 'Cubic'
if split:
if udim is not None:
if udim in solution.baketile[node.label].keys():
bake_img.image = solution.baketile[node.label][udim][2]#.copy()
#bake_img.image.pixels = solution.baketile[node.label][udim][2].pixels[:]
else:
bake_img.image = bpy.data.images["bw_default_orig"]
else:
bake_img.image = solution.bakepass[node.label].sbake_result#.copy()
#bake_img.image.pixels = solution.bakepass[node.label].sbake_result.pixels[:]
else:
if udim is not None:
if udim in solution.baketile[node.label].keys():
bake_img.image = solution.baketile[node.label][udim][0]#.copy()
#bake_img.image.pixels = solution.baketile[node.label][udim][0].pixels[:]
else:
bake_img.image = bpy.data.images["bw_default_orig"]
else:
bake_img.image = solution.bakepass[node.label].bake_result#.copy()
#bake_img.image.pixels = solution.bakepass[node.label].bake_result.pixels[:]
bake_img.image.scale(solution.format['img_xres'], solution.format['img_yres'])
mask_img = None
if node.inputs['Mask'].is_linked:
if bake_set['use_mask'] or solution.format['marginer']:
mask_img = node.inputs['Mask'].links[0].from_node
bake_set = solution.bakepass[node.label].get_settings()
if split:
if udim is not None:
if udim in solution.baketile[node.label]:
mask_img.image = solution.baketile[node.label][udim][3]
else:
mask_img.image = bpy.data.images["bw_default_orig"]
else:
mask_img.image = solution.bakepass[node.label].smask_result
else:
if udim is not None:
if udim in solution.baketile[node.label]:
mask_img.image = solution.baketile[node.label][udim][1]
else:
mask_img.image = bpy.data.images["bw_default_orig"]
else:
mask_img.image = solution.bakepass[node.label].mask_result
mask_img.image.scale(solution.format['img_xres'], solution.format['img_yres'])
masks.append(node)
else:
links.remove(node.inputs['Mask'].links[0])
# Add all masks together
add_masks(masks, nodes, solution.postproc.node_tree.links)
# Bake post material
err += bake_post_material(post_color, post_alpha, solution.postproc, post_mask, bake_set['use_mask'])
# Apply alpha channel if needed
post_combined = None
if post_alpha:
_print("> -Creating alpha channel", tag=True, wrap=False)
post_combined = bpy.data.images.new(output_name + "combined", solution.format['img_xres'], solution.format['img_yres'])
bw_solution_data['images'].append(post_combined)
post_combined.colorspace_settings.name = solution.format['img_color_space']
post_combined.use_generated_float = solution.format['img_use_float']
post_combined.alpha_mode = 'CHANNEL_PACKED'
aerr = alpha_pass(post_color, post_alpha, post_combined)
if not aerr:
_print("", tag=True)
err += aerr
# Switch into output scene and apply output format
bpy.context.window.scene = output_scene
apply_output_format(output_scene.render.image_settings, solution.format)
solution_image = post_color
if post_combined: solution_image = post_combined
# Do final processing if needed
if solution.format['marginer']: paint_margin(solution_image, post_mask, solution.format['marginer_size'], solution.format['marginer_fill'])
if solution.format['fast_aa']: fast_aa(solution_image, solution.format['fast_aa_lvl'])
# Save the image to disk
_print("> -Writing changes to %s" % (output_file), tag=True)
_print(">", tag=True)
solution_image.save_render(output_file, scene=output_scene)
images_saved.append(output_file)
bpy.context.window.scene = base_scene
return err
# Takes a bake pass node and output format, creating an image in the bake_result and mask_result (if used)
def process_bake_pass(solution, key, split_list=None, exclude=True, imgs=None):
err = False
no_split = True
node = solution.bakepass[key]
format = solution.format
udim = format['img_udim']
vcol = True if 'vcol' in format.keys() else False
# Gather pass settings
bake_settings = node.get_settings()
bake_settings['node_name'] = node.name
bake_settings['vcol'] = vcol
bake_meshes = node.get_inputs()
# Generate the bake and mask images
def pbp_img_gen(imgs=None, node=None, bake_settings=None):
if imgs is None or (len(imgs) and imgs[0] is None):
if imgs is None: _print(" [Mesh Nodes (%i)]" % (len(bake_meshes)), tag=True)
img_bake = bpy.data.images.new(node.get_name(), width=bake_settings["x_res"], height=bake_settings["y_res"])
bw_solution_data['images'].append(img_bake)
img_mask = None
img_bake.alpha_mode = 'NONE'
if format['img_use_float']:
img_bake.use_generated_float = True
img_bake.colorspace_settings.name = format['img_color_space']
#img_bake.colorspace_settings.name = 'Linear'
if 'sRGB' in format['img_color_space']:
bake_settings['osl_curv_srgb'] = True
if bake_settings['use_bg_col']:
img_bake.generated_color = bake_settings['bg_color']
else:
if (bake_settings['bake_type'] in ['NORMAL', 'TEXNORM', 'OBJNORM', 'BEVNORMEMIT', 'BEVNORMNORM', 'CLEARNORM', 'OSL_BENTNORM'] and bake_settings['norm_s'] == 'TANGENT') or (bake_settings['bake_type'] == 'MULTIRES' and bake_settings['multi_pass'] == 'NORMALS'):
img_bake.generated_color = (0.5, 0.5, 1.0, 1.0)
elif bake_settings['bake_type'] == 'CURVATURE':
img_bake.generated_color = (bake_settings["curv_mid"], bake_settings["curv_mid"], bake_settings["curv_mid"], 1.0)
elif bake_settings['bake_type'] == 'OSL_HEIGHT':
img_bake.generated_color = (bake_settings["osl_height_midl"], bake_settings["osl_height_midl"], bake_settings["osl_height_midl"], 1.0)
#if bake_settings['use_mask']:
img_mask = bpy.data.images.new("mask_" + node.get_name(), width=bake_settings["x_res"], height=bake_settings["y_res"])
bw_solution_data['images'].append(img_mask)
img_mask.alpha_mode = 'NONE'
img_mask.colorspace_settings.name = format['img_non_color']
img_mask.colorspace_settings.is_data = True
else:
img_bake = imgs[0]
img_mask = None
#if bake_settings['use_mask'] and len(imgs) > 1:
if len(imgs) > 1:
img_mask = imgs[1]
return img_bake, img_mask
if not vcol:
img_bake, img_mask = pbp_img_gen(imgs=imgs, node=node, bake_settings=bake_settings)
else:
_print(" [Mesh Nodes (%i)]" % (len(bake_meshes)), tag=True)
img_bake = img_mask = None
# Begin processing bake meshes
for mesh_dat in bake_meshes:
mesh = mesh_dat[0]
multi = (bake_settings['bake_type'] == 'MULTIRES')
# Determine bake type and object groups
def pbp_type_and_obj_groups(mesh=None):
hi2lo = False
matbk = False
bbbk = False
bake_settings['bbbk'] = False
if mesh.bl_idname == 'BakeWrangler_Sort_Meshes':
input_src = get_input(mesh_dat[1])
mesh_settings = mesh.get_settings(input=mesh_dat[1])
mesh_settings['mesh_name'] = input_src.name
mesh_settings['mesh_label'] = input_src.get_name()
if input_src.bl_idname == 'BakeWrangler_Bake_Material':
hi2lo = False
matbk = True
active_meshes = input_src.get_materials()
selected_objs = []
scene_objs = []
else:
hi2lo = True
active_meshes = mesh.get_objects('TARGET', mesh_dat[1])
selected_objs = mesh.get_objects('SOURCE', mesh_dat[1])
scene_objs = mesh.get_objects('SCENE', mesh_dat[1])
else:
mesh_settings = mesh.get_settings()
mesh_settings['mesh_name'] = mesh.name
mesh_settings['mesh_label'] = mesh.get_name()
if mesh.bl_idname == 'BakeWrangler_Bake_Material':
matbk = True
active_meshes = mesh.get_materials()
selected_objs = []
scene_objs = []
else:
if mesh.bl_idname == 'BakeWrangler_Bake_Billboard':
bbbk = True
bake_settings['bbbk'] = True
active_meshes = mesh.get_objects('TARGET')
selected_objs = mesh.get_objects('SOURCE')
scene_objs = mesh.get_objects('SCENE')
return hi2lo, matbk, bbbk, mesh_settings, active_meshes, selected_objs, scene_objs
hi2lo, matbk, bbbk, mesh_settings, active_meshes, selected_objs, scene_objs = pbp_type_and_obj_groups(mesh=mesh)
bake_settings['bbbk'] = bbbk
# Bake is supposed to be split into multiple files based on object names from list
if split_list:
active_split = []
for act in active_meshes:
msh = act
if hi2lo:
replace_list = []
for splt in split_list:
replace_list.append(splt[0:3])
split_list = replace_list
msh = act[0]
if (exclude and msh not in split_list) or (not exclude and msh in split_list):
active_split.append(act)
if len(active_split):
active_meshes = active_split
no_split = False
else:
continue
if matbk:
_print("> Materials: [%s] [Targets (%i)]" % (mesh_settings['mesh_label'], len(active_meshes)), tag=True)
elif bbbk:
_print("> Billboard: [%s]" % (mesh_settings['mesh_label']), tag=True)
else:
_print("> Mesh: [%s] [Targets (%i)]" % (mesh_settings['mesh_label'], len(active_meshes)), tag=True)
if mesh_settings['margin'] > 0 or mesh_settings['margin_auto']:
if mesh_settings['margin_auto']:
mesh_settings["margin"] = min(bake_settings["x_res"], bake_settings["y_res"]) / 256
if mesh_settings["margin"] < 1: mesh_settings["margin"] = 1
if mesh_settings["margin"] > 32: mesh_settings["margin"] = 32
# Recalculate margin size based on ratio between bake and output images
bake_ratio = math.sqrt(math.pow(bake_settings["x_res"], 2) + math.pow(bake_settings["y_res"], 2)) # Pythag for diagonal
outp_ratio = math.sqrt(math.pow(format['img_xres'], 2) + math.pow(format['img_yres'], 2))
margin_ratio = bake_ratio / outp_ratio # Ratio between sizes
mesh_settings["margin"] *= margin_ratio # Multiply margin by ratio to maintain size in output
if debug: _print("> Margin %s" % (mesh_settings["margin"]), tag=True)
# Process each active mesh
for active in active_meshes:
# Unpack active and selected from active if doing high to low poly auto mapped bake
if hi2lo:
scene_objs = active[2]
selected_objs = active[1]
active = active[0]
# Load in template bake scene with mostly optimized settings for baking
bake_scene_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources", "BakeWrangler_Scene.blend")
with bpy.data.libraries.load(bake_scene_path, link=False, relative=False) as (file_from, file_to):
file_to.scenes.append("BakeWrangler")
bake_scene = file_to.scenes[0]
bake_scene.name = "bake_" + node.get_name() + "_" + mesh.get_name() + "_" + active[0].name
bake_scene.frame_current = current_frame
if anim_seed:
bake_scene.cycles.seed = current_frame
bw_solution_data['scenes'].append(bake_scene)
# Copy render settings if required
if bake_settings['cpy_render']:
if bake_settings['cpy_from'] in [None, ""]:
bake_settings['cpy_from'] = active_scene
copy_render_settings(bake_settings['cpy_from'], bake_scene)
# Set up camera if that is ray origin
if 'view_from' in mesh_settings:
bake_scene.collection.objects.link(mesh_settings['view_from'])
bake_scene.camera = mesh_settings['view_from']
bake_scene.render.bake.view_from = 'ACTIVE_CAMERA'
else:
bake_scene.render.bake.view_from = 'ABOVE_SURFACE'
# Set the device and sample count to override anything that could have been copied
bake_OSL = False
bake_scene.cycles.device = bake_settings['bake_device']
if bake_settings['bake_type'].startswith('OSL_') or (mesh_settings['material_replace'] and mesh_settings['material_osl']):
if bake_settings['bake_type'] == 'OSL_HEIGHT':
bake_OSL = True
bake_scene.cycles.device = 'CPU'
bake_scene.cycles.shading_system = True
bake_scene.cycles.samples = bake_settings['bake_samples']
#bake_scene.cycles.aa_samples = bake_settings['bake_samples']
bake_scene.cycles.use_adaptive_sampling = bake_settings['bake_usethresh']
bake_scene.cycles.adaptive_threshold = bake_settings['bake_threshold']
bake_scene.cycles.time_limit = bake_settings['bake_timelimit']
# Set custom world instead of default if enabled
if bake_settings['use_world']:
if bake_settings['the_world'] not in [None, ""]:
bake_scene.world = bake_settings['the_world']
else:
bake_scene.world = active_scene.world
# Initialise bake type settings
mesh_settings["cage"] = False
mesh_settings["cage_object"] = None
mesh_settings["cage_obj_name"] = ""
to_active = False
selected = None
# For material bake, we can skip most of the rest of the steps
if matbk:
# Add a plane, sized correctly and put a copy of the material on it after being prepped
matpln = material_plane.copy()
bw_solution_data['objects'].append(matpln)
matpln.data = material_plane.data.copy()
matpln.name = "mat_plane_" + active[0].name
matpln.dimensions = (mesh_settings['matbk_width'], mesh_settings['matbk_height'], 0.0)
# Do material copy and prep
mat2bk = active[0].copy()
bw_solution_data['materials'].append(mat2bk)
matpln.data.materials.append(mat2bk)
bake_scene.collection.objects.link(matpln)
# Switch into bake scene
bpy.context.window.scene = bake_scene
# Select the target and make it active
bpy.ops.object.select_all(action='DESELECT')
matpln.select_set(True)
bpy.context.view_layer.objects.active = matpln
# Bake the plane
err += bake_solo(img_bake, {'mat': mat2bk}, bake_settings, mesh_settings)
# Switch back to main scene before next pass. Nothing will be deleted so that the file can be examined for debugging.
bpy.context.window.scene = base_scene
# Skip the rest of the steps
continue
# Determine what strategy to use for this bake
if not multi:
# See if there are valid source objects to do a selected to active bake
for obj in selected_objs:
# Let a duplicate of the target object count if they use different UV Maps
if obj[0] != active[0] or (len(obj) > 1 and len(active) > 1 and obj[1] != active[1]):
to_active = True
break
if mesh_settings["bake_mods"]: to_active = True
# Copy all selected objects over if 'to active' pass
if to_active:
selected = bpy.data.collections.new("Selected_" + active[0].name)
bw_solution_data['collections'].append(selected)
if mesh_settings["bake_mods"]:
# Add the Target to the Sources if it isn't already in the list before processing
active_in_selected = False
for obj in selected_objs:
if obj[0] == active[0]:
active_in_selected = True
break
if not active_in_selected:
selected_objs.append(active)
for obj in selected_objs:
# Let a duplicate of the target object in if they use different UV Maps
if obj[0] != active[0] or (len(obj) > 1 and len(active) > 1 and obj[1] != active[1]) or mesh_settings["bake_mods"]:
copy = prep_object_for_bake(obj[0], invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]])
bw_solution_data['objects'].append(copy)
selected.objects.link(copy)
# Set UV map to use if one was selected
if len(obj) > 1 and obj[1] not in [None, ""]:
copy.data.uv_layers.active = copy.data.uv_layers[obj[1]]
bake_scene.collection.children.link(selected)
# Add the cage copy to the scene because it doesn't work properly in a different scene currently
if len(active) > 2 and active[2]:
mesh_settings["cage"] = True
mesh_settings["cage_object"] = prep_object_for_bake(active[2], invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]])
bw_solution_data['objects'].append(mesh_settings["cage_object"])
mesh_settings["cage_obj_name"] = mesh_settings["cage_object"].name
elif active[0].bw_auto_cage:
mesh_settings["cage"] = True
mesh_settings["cage_object"] = prep_object_for_bake(active[0].bw_auto_cage, invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]])
bw_solution_data['objects'].append(mesh_settings["cage_object"])
mesh_settings["cage_obj_name"] = mesh_settings["cage_object"].name
else:
# Collection of base objects for multi-res to link into
base_col = bpy.data.collections.new("Base_" + active[0].name)
bw_solution_data['collections'].append(base_col)
bake_scene.collection.children.link(base_col)
# Regardless of strategy the following data will be used. Copies are made so other passes can get the originals
if not multi:
if mesh_settings["bake_mods"]:
target = prep_object_for_bake(active[0], strip=True, invert=mesh_settings["bake_mods_invert"], vcols=[vcol, active[0]])
else:
target = prep_object_for_bake(active[0], vcols=[vcol, active[0]])
else:
active_obj = active[0]
target = active_obj.copy()
target.data = active_obj.data.copy()
bw_solution_data['objects'].append(target)
# Check for valid cage now if one is set
if mesh_settings["cage"]:
if len(target.data.polygons) != len(mesh_settings["cage_object"].data.polygons):
_print("> !Cage invalid for [%s]" % (active[0].name), tag=True)
mesh_settings["cage"] = False
mesh_settings["cage_obj_name"] = ""
# Set UV map to use if one was selected
if len(active) > 1 and active[1] not in [None, ""]:
target.data.uv_layers.active = target.data.uv_layers[active[1]]
# Materials should be removed from the target copy for To active
if to_active:
target.data.materials.clear()
target.data.polygons.foreach_set('material_index', [0] * len(target.data.polygons))
target.data.update()
# If no specific cage, but auto cage is enabled, create a cage for the object
if not mesh_settings["cage"] and mesh_settings["auto_cage"]:
mesh_settings["cage"] = True
mesh_settings["cage_object"] = target.copy()
mesh_settings["cage_object"].data = target.data.copy()
mesh_settings["cage_object"].name = "%s.%s" % (target.name, "auto_cage")
mesh_settings["cage_obj_name"] = mesh_settings["cage_object"].name
# Expand the cage object
cage_displace = mesh_settings["cage_object"].modifiers.new("cage_disp", 'DISPLACE')
cage_displace.strength = mesh_settings['acage_expansion']
cage_displace.direction = 'NORMAL'
cage_displace.mid_level = 0.0
# Smooth normals
mesh_settings["cage_object"].data.use_auto_smooth = True
mesh_settings["cage_object"].data.auto_smooth_angle = mesh_settings['acage_smooth']
for poly in mesh_settings["cage_object"].data.polygons:
poly.use_smooth = True
# Clear sharps
for edge in mesh_settings["cage_object"].data.edges:
edge.use_edge_sharp = False
# Add target before doing mats
if not to_active or bake_OSL:
bake_scene.collection.objects.link(target)
# Replace materials if required
if bake_settings['bake_cat'] == 'WRANG' or mesh_settings['material_replace']:
replace_materials_for_shader_bake(bake_scene, bake_settings, mesh_settings)
# Add OSL materials to target now so they can be configured by the next step
if bake_settings['bake_type'] == 'OSL_HEIGHT':
target.data.materials.append(osl_height)
# Create unique copies for every material in the scene before anything else is done
unique_mats = make_materials_unique_to_scene(bake_scene, "_" + node.name + "_" + mesh.name + "_" + active[0].name, bake_settings)
for mkey in unique_mats.keys():
bw_solution_data['materials'].append(unique_mats[mkey])
# Add target after doing mats
if to_active and not bake_OSL:
bake_scene.collection.objects.link(target)
# Make sure a basic material is on the target
if bbbk:
# Modify settings for billboard bake here to use glossy color pass with special material
basic_mat = billboard_mat.copy()
bw_solution_data['materials'].append(basic_mat)
basic_mat.name = billboard_mat.name + node.name + mesh.name
check_has_material(target, None, basic_mat)
else:
basic_mat = bpy.data.materials.new(name="bw_basic_" + node.name + "_" + mesh.name)
bw_solution_data['materials'].append(basic_mat)
basic_mat.use_nodes = True
check_has_material(target, unique_mats, basic_mat)
# Add a material to any objects lacking a material for bent norms
if bake_settings['bake_type'] == 'OSL_BENTNORM':
bent_mat = basic_mat.copy()
bw_solution_data['materials'].append(bent_mat)
bent_mat.name = "bw_bent_" + node.name + "_" + mesh.name
if to_active:
for from_obj in selected.objects:
check_has_material(from_obj, unique_mats, bent_mat)
# Rotation of target
bake_settings['bb_rot'] = active[0].rotation_euler
# Copy all scene objects over if not a multi-res pass
if not multi:
scene = bpy.data.collections.new("Scene_" + active[0].name)
bw_solution_data['collections'].append(scene)
for obj in scene_objs:
# Exclude current target if its in the list as well as any source objects as these
# cause problems in blender 3.1
dups = [active[0]]
for dup in selected_objs:
dups.append(dup[0])
if obj[0] not in dups:
scene.objects.link(obj[0])
bake_scene.collection.children.link(scene)
# Add cage object
if mesh_settings["cage"]:
bake_scene.collection.objects.link(mesh_settings["cage_object"])
# Switch into bake scene
bpy.context.window.scene = bake_scene
# OSL setup for cage, swaps cage for target
if (bake_OSL or bbbk) and mesh_settings["cage_object"]:
target.hide_render = True
mesh_settings["cage_object"].data.materials.clear()
mesh_settings["cage_object"].data.polygons.foreach_set('material_index', [0] * len(target.data.polygons))
mesh_settings["cage_object"].data.update()
mesh_settings["cage_object"].data.materials.append(target.data.materials[0])
target = mesh_settings["cage_object"]
mesh_settings["cage"] = False
# Select the target and make it active
bpy.ops.object.select_all(action='DESELECT')
target.select_set(True)
bpy.context.view_layer.objects.active = target
# If UDIM tiles are enabled, the bake must repeat for each tile and the UV map needs to be modified between
if udim:
tiles = solution.baketile[key]
udims = uv_to_udim(solution, key, target.data.uv_layers.active)
_print("> UDIM: [%s] [Tiles (%s)]" % (active[0].name, len(udims)), tag=True)
# Do pre bake set up before doing each tile
if multi:
bake_multi_res(img_bake, unique_mats, bake_settings, mesh_settings, base_col, target, active_obj, pre_only=True)
elif to_active and not bake_OSL:
err += bake_to_active(img_bake, unique_mats, bake_settings, mesh_settings, selected, pre_only=True)
else:
err += bake_solo(img_bake, unique_mats, bake_settings, mesh_settings, pre_only=True)
for tile in udims:
if not len(tiles[tile]):
# Tile image hasn't been assigned yet, make a copy of the bake image (and mask)
tile_bake = img_bake.copy()
bw_solution_data['images'].append(tile_bake)
tile_bake.pixels = img_bake.pixels[:]
tile_bake.name = "%s.%s" % (img_bake.name, tile)
tile_bake.update()
tile_mask = None
if bake_settings['use_mask'] or format['marginer']:
tile_mask = img_mask.copy()
bw_solution_data['images'].append(tile_mask)
tile_mask.pixels = img_mask.pixels[:]
tile_mask.name = "%s.%s" % (img_mask.name, tile)
tile_mask.update()
tiles[tile] = [tile_bake, tile_mask]
tile_bake = tiles[tile][0]
if split_list and not exclude:
tile_bake = tiles[tile][0].copy()
bw_solution_data['images'].append(tile_bake)
tile_bake.pixels = tiles[tile][0].pixels[:]
tile_bake.update()
if len(tiles[tile]) < 3:
tiles[tile].append(tile_bake)
else:
tiles[tile][2] = tile_bake
# UV map needs to be modified to move the current tile within the 0-1 range
uv_name = target.data.uv_layers.active.name
uv_tile = target.data.uv_layers[uv_name]
focus_udim_tile(uv_tile, tile)
bake_settings['tile_no'] = tile
# Perform bake type needed
if multi:
err += bake_multi_res(tile_bake, unique_mats, bake_settings, mesh_settings, base_col, target, active_obj, False, tile_bake)
elif to_active and not bake_OSL:
err += bake_to_active(tile_bake, unique_mats, bake_settings, mesh_settings, selected, False, tile_bake)
else:
err += bake_solo(tile_bake, unique_mats, bake_settings, mesh_settings, False, tile_bake)
# Switch back to original UVs before next pass
focus_udim_tile(uv_tile, tile, unfocus=True)
# Go back over tiles if masks are needed
if bake_settings['use_mask'] or format['marginer']:
bake_mask(tile_mask, unique_mats, bake_settings, mesh_settings, to_active, target, selected, pre_only=True)
for tile in udims:
tile_mask = tiles[tile][1]
if split_list and not exclude:
tile_mask = tiles[tile][1].copy()
bw_solution_data['images'].append(tile_mask)
tile_mask.pixels = tiles[tile][1].pixels[:]
tile_mask.update()
if len(tiles[tile]) < 4:
tiles[tile].append(tile_mask)
else:
tiles[tile][3] = tile_mask
# UV map needs to be modified to move the current tile within the 0-1 range
uv_name = target.data.uv_layers.active.name
uv_tile = target.data.uv_layers[uv_name]
focus_udim_tile(uv_tile, tile)
bake_settings['tile_no'] = tile
# Set samples to the mask value
bake_scene.cycles.device = bake_settings['bake_device']
bake_scene.cycles.samples = 1
#bake_scene.cycles.aa_samples = 1
err += bake_mask(tile_mask, unique_mats, bake_settings, mesh_settings, to_active, target, selected, False, tile_mask)
# Switch back to original UVs before next pass
focus_udim_tile(uv_tile, tile, unfocus=True)
else:
if vcol:
img_vcol = target.data.color_attributes.new(node.get_name(), format['vcol_type'], format['vcol_domain'])
target.data.color_attributes.active_color = img_vcol
# Perform bake type needed
if multi:
if vcol: _print("> Multi-Resolution cannot be output to vertex colors, skipping pass", tag=True)
else: err += bake_multi_res(img_bake, unique_mats, bake_settings, mesh_settings, base_col, target, active_obj)
elif to_active and not bake_OSL:
err += bake_to_active(img_bake, unique_mats, bake_settings, mesh_settings, selected)
else:
err += bake_solo(img_bake, unique_mats, bake_settings, mesh_settings)
# Bake the mask if samples are non zero
if (bake_settings['use_mask'] or format['marginer']) and not vcol:
# Set samples to the mask value
bake_scene.cycles.device = bake_settings['bake_device']
bake_scene.cycles.samples = 1
#bake_scene.cycles.aa_samples = 1
err += bake_mask(img_mask, unique_mats, bake_settings, mesh_settings, to_active, target, selected)
# Output vcolors to vcol object copy
if vcol:
if active[0].name not in solution.bakecols.keys():
# Create dict for this object if not done already
solution.bakecols[active[0].name] = {}
# Store name of vcols for this bake node
solution.bakecols[active[0].name][node.name] = img_vcol.name
# Copy vcols to base object copy
copy_vert_cols(name=img_vcol.name, cpy_from=target, cpy_to=active[0].bw_vcols)
# Switch back to main scene before next pass. Nothing will be deleted so that the file can be examined for debugging.
bpy.context.window.scene = base_scene
# Return early if no split list item was baked
if split_list and no_split:
if not exclude:
return -1
else:
return err
# Finished inputs, return the bakes
if not udim and not vcol:
if exclude:
node.bake_result = img_bake
node.mask_result = img_mask
else:
node.sbake_result = img_bake
node.smask_result = img_mask
elif vcol:
node.vcol_result = img_vcol
return err
# Bake a multi-res pass
def bake_multi_res(img_bake, materials, bake_settings, mesh_settings, base_col, target, original, pre_only=False, tile=None):
# Set multi res levels on copy
multi_mod = None
if not tile: # Tile is assumed to have run a pre only time first
for mod in target.modifiers:
if mod.type == 'MULTIRES':
multi_mod = mod
break
if multi_mod:
multi_mod.levels = 0
multi_mod.render_levels = multi_mod.total_levels
if bake_settings["multi_samp"] == 'FROMMOD':
src_mod = None
for mod in original.modifiers:
if mod.type == 'MULTIRES':
src_mod = mod
break
if src_mod:
multi_mod.levels = src_mod.levels
multi_mod.render_levels = src_mod.render_levels
elif bake_settings["multi_samp"] == 'CUSTOM':
if bake_settings["multi_targ"] >= 0 and bake_settings["multi_targ"] <= multi_mod.total_levels:
multi_mod.levels = bake_settings["multi_targ"]
if bake_settings["multi_sorc"] >= 0 and bake_settings["multi_sorc"] <= multi_mod.total_levels:
multi_mod.render_levels = bake_settings["multi_sorc"]
# Next link all the objects from the base scene to hopefully stop any modifier errors
for obj in base_scene.objects:
base_col.objects.link(obj)
obj.select_set(False)
# Add a bake target image node to each material
for mat in materials.values():
if not tile:
if debug: _print("> Preparing material [%s] for [Multi-Res %s] bake" % (mat.name, bake_settings['bake_type']), tag=True)
image_node = mat.node_tree.nodes.new("ShaderNodeTexImage")
image_node.image = img_bake
image_node.select = True
mat.node_tree.nodes.active = image_node
else:
# In the case of a tile, the node should already be created and active, so just change image path
image_node = mat.node_tree.nodes.active
image_node.image = tile
# Bake it
if pre_only: return 0
return bake(bake_settings['bake_cat'], bake_settings['multi_pass'], bake_settings, mesh_settings, False, True)
# Bake a to-active pass
def bake_to_active(img_bake, materials, bake_settings, mesh_settings, selected, pre_only=False, tile=None):
if not tile:
# Make the source objects selected
if not bake_settings['bbbk']:
for obj in selected.objects:
obj.select_set(True)
# Add texture node set up to target object
mat = bpy.context.view_layer.objects.active.material_slots[0].material
image_node = mat.node_tree.nodes.new("ShaderNodeTexImage")
image_node.image = img_bake
image_node.select = True
mat.node_tree.nodes.active = image_node
# Prepare the materials for the bake type
for mat in materials.values():
if debug: _print("> Preparing material [%s] for [%s] bake" % (mat.name, bake_settings['bake_type']), tag=True)
if not mat.name.startswith('bw_basic_'): prep_material_for_bake(mat.node_tree, bake_settings['bake_type'], bake_settings)
else:
# In the case of a tile, the node should already be created and active, so just change image path
mat = bpy.context.view_layer.objects.active.material_slots[0].material
image_node = mat.node_tree.nodes.active
image_node.image = tile
# Bake it
if pre_only: return 0
return bake(bake_settings['bake_cat'], bake_settings['bake_type'], bake_settings, mesh_settings, True, False)
# Bake single object pass
def bake_solo(img_bake, materials, bake_settings, mesh_settings, pre_only=False, tile=None):
# Prepare the materials for the bake type
for mat in materials.values():
if not tile:
if debug: _print("> Preparing material [%s] for [%s] bake" % (mat.name, bake_settings['bake_type']), tag=True)
prep_material_for_bake(mat.node_tree, bake_settings['bake_type'], bake_settings)
# For non To active bakes, add an image node to the material and make it selected + active for bake image
image_node = mat.node_tree.nodes.new("ShaderNodeTexImage")
image_node.image = img_bake
image_node.select = True
mat.node_tree.nodes.active = image_node
else:
# In the case of a tile, the node should already be created and active, so just change image path
image_node = mat.node_tree.nodes.active
image_node.image = tile
# Bake it
if pre_only: return 0
return bake(bake_settings['bake_cat'], bake_settings['bake_type'], bake_settings, mesh_settings, False, False)
# Bake a masking pass
def bake_mask(img_mask, materials, bake_settings, mesh_settings, to_active, target, selected, pre_only=False, tile=None):
if not tile:
# Make sure a basic material is on every object
mat = bpy.data.materials.new(name="bw_mask_" + bake_settings["node_name"] + "_" + mesh_settings["mesh_name"])
bw_solution_data['materials'].append(mat)
mat.use_nodes = True
objs = [target]
if selected:
for obj in selected.objects:
objs.append(obj)
for obj in objs:
check_has_material(obj, materials, mat)
# Requires adding a pure while emit shader to all the materials first and changing target image
for mat in materials.values():
if not tile:
prep_material_for_bake(mat.node_tree, 'MASK', bake_settings)
# Add image node to material and make it selected + active
if not to_active:
if not tile:
image_node = mat.node_tree.nodes.new("ShaderNodeTexImage")
image_node.image = img_mask
image_node.select = True
mat.node_tree.nodes.active = image_node
else:
# In the case of a tile, the node should already be created and active, so just change image path
image_node = mat.node_tree.nodes.active
image_node.image = tile
# Add image node to target and make it selected + active (should only be one material at this point)
if to_active:
if not tile:
image_node = bpy.context.view_layer.objects.active.material_slots[0].material.node_tree.nodes.new("ShaderNodeTexImage")
image_node.image = img_mask
image_node.select = True
bpy.context.view_layer.objects.active.material_slots[0].material.node_tree.nodes.active = image_node
else:
# In the case of a tile, the node should already be created and active, so just change image path
image_node = bpy.context.view_layer.objects.active.material_slots[0].material.node_tree.nodes.active
image_node.image = tile
# Bake it
#mesh_settings["margin"] += mesh_settings["mask_margin"]
if pre_only: return 0
return bake('PBR', 'MASK', bake_settings, mesh_settings, to_active, False)
# Call actual bake commands
def bake(bake_cat, bake_type, bake_settings, mesh_settings, to_active, multi):
# Set 'real' bake pass. PBR use EMIT rather than the named pass, since those passes don't exist.
if bake_settings['bbbk']:
real_bake_type = 'GLOSSY'
bake_settings['influences'] = set()
bake_settings['influences'].add('DIRECT')
bpy.context.scene.cycles.transparent_max_bounces = mesh_settings['alpha_bounce']
to_active = False
elif bake_cat in ['PBR', 'WRANG']:
if bake_type in ['OBJNORM', 'BEVNORMNORM', 'OSL_BENTNORM']: real_bake_type = 'NORMAL'
else: real_bake_type = 'EMIT'
elif bake_cat == 'CORE' and bake_type == 'SMOOTHNESS':
real_bake_type = 'ROUGHNESS'
else:
real_bake_type = bake_type
# Set target of output
target_mode = 'VERTEX_COLORS' if ('vcol' in bake_settings.keys() and bake_settings['vcol']) else 'IMAGE_TEXTURES'
# Set threads if not default
if bake_settings['threads'] != 0:
bpy.context.scene.render.threads_mode = 'FIXED'
bpy.context.scene.render.threads = bake_settings['threads']
# Set tile sizes if not default
if bake_settings['tiles'] in ['IMG', 'CUST']:
if bake_settings['tiles'] == 'IMG':
bpy.context.scene.cycles.tile_size = bake_settings["x_res"]
else:
bpy.context.scene.cycles.tile_size = bake_settings["tile_size"]
if mesh_settings["margin_extend"]: bpy.context.scene.render.bake.margin_type = 'EXTEND'
else: bpy.context.scene.render.bake.margin_type = 'ADJACENT_FACES'
if debug: _print("> Real bake type set to [%s], Mode [%s]" % (real_bake_type, target_mode), tag=True)
# Update view layer to be safe
bpy.context.view_layer.update()
start = datetime.now()
if 'tile_no' in bake_settings.keys():
_print("> -Baking %s pass [tile %s]: " % (bake_type, bake_settings['tile_no']), tag=True, wrap=False)
del bake_settings['tile_no']
else:
_print("> -Baking %s pass: " % (bake_type), tag=True, wrap=False)
# Do the bake. Most of the properties can be passed as arguments to the operator.
err = False
try:
if not multi:
bpy.ops.object.bake(
type=real_bake_type,
pass_filter=bake_settings["influences"],
margin=int(mesh_settings["margin"]),
use_selected_to_active=to_active,
max_ray_distance=mesh_settings["max_ray_dist"],
cage_extrusion=mesh_settings["ray_dist"],
cage_object=mesh_settings["cage_obj_name"],
normal_space=bake_settings["norm_s"],
normal_r=bake_settings["norm_r"],
normal_g=bake_settings["norm_g"],
normal_b=bake_settings["norm_b"],
target=target_mode,
save_mode='INTERNAL',
use_clear=False,
use_cage=mesh_settings["cage"],
)
else:
bpy.context.scene.render.use_bake_multires = True
bpy.context.scene.render.bake_margin = int(mesh_settings["margin"])
bpy.context.scene.render.bake_type = bake_type
bpy.context.scene.render.use_bake_clear = False
bpy.ops.object.bake_image()
except RuntimeError as error:
_print("%s" % (error), tag=True)
err = True
else:
_print("Completed in %s" % (str(datetime.now() - start)), tag=True)
return err
# Handle post processes that need to be applied to the baked data to create the desired map
def bake_post(img_bake, settings, format):
post_obj = post_scene.objects["BW_Post_%s" % (settings['bake_type'])]
post_mat = post_obj.material_slots[0].material.node_tree.nodes
post_src = post_mat["bw_post_input"]
post_out = post_mat["bw_post_output"]
post_img = bpy.data.images.new(img_bake.name + "_POST", width=settings["x_res"], height=settings["y_res"])
post_img.colorspace_settings.name = format['img_color_space']
post_img.colorspace_settings.is_data = img_bake.colorspace_settings.is_data
post_img.use_generated_float = img_bake.use_generated_float
# Switch into post scene and set up the selection state
bpy.context.window.scene = post_scene
bpy.ops.object.select_all(action='DESELECT')
post_obj.select_set(True)
bpy.context.view_layer.objects.active = post_obj
# Set up standard images
post_src.image = img_bake
post_out.image = post_img
# Generate image
bpy.context.view_layer.update()
err = False
try:
bpy.ops.object.bake(
type="EMIT",
save_mode='INTERNAL',
use_clear=False,
)
except RuntimeError as error:
_print(": %s" % (error), tag=True)
err = True
else:
pass
return [err, post_img]
# Bake post processing step for a vcol output
def bake_post_vcols(post_obj, post_mat, solution):
# Add copy of object to post scene
post_cpy = post_obj.copy()
post_cpy.data = post_obj.data.copy()
post_scene.collection.objects.link(post_cpy)
# Clear materials and set it to use post material
post_cpy.data.materials.clear()
post_cpy.data.polygons.foreach_set('material_index', [0] * len(post_cpy.data.polygons))
post_cpy.data.update()
post_cpy.data.materials.append(post_mat)
# Add vertex color slot to bake into
vcol = post_cpy.data.color_attributes.new(post_mat.name, solution.format['vcol_type'], solution.format['vcol_domain'])
post_cpy.data.color_attributes.active_color = vcol
# Switch into scene and set up selection
bpy.context.window.scene = post_scene
bpy.ops.object.select_all(action='DESELECT')
post_cpy.select_set(True)
bpy.context.view_layer.objects.active = post_cpy
# Generate output
bpy.context.view_layer.update()
_print("> -Performing post bake processing", tag=True, wrap=False)
err = False
try:
bpy.ops.object.bake(
type="EMIT",
target='VERTEX_COLORS',
save_mode='INTERNAL',
)
except RuntimeError as error:
_print(": %s" % (error), tag=True)
err = True
else:
_print("", tag=True)
return [err, post_cpy, vcol.name]
# Perform bake of a post processing material shader
def bake_post_material(post_col, post_alp, post_mat, post_msk, masked):
post_obj = post_scene.objects["BW_Post"]
post_obj.material_slots[0].material = post_mat
post_mat_nodes = post_mat.node_tree.nodes
post_out_col = post_mat_nodes["bw_post_output"]
post_out_alp = post_mat_nodes["bw_post_output_alpha"]
post_out_msk = post_mat_nodes["bw_post_output_mask"]
output_switch = post_mat_nodes["AlphaSwitch"].inputs["AlphaSwitch"]
mask_switch = post_mat_nodes["AlphaSwitch"].inputs["MaskSwitch"]
use_mask = post_mat_nodes["AlphaSwitch"].inputs["UseMask"]
# Switch into scene and set up selection
bpy.context.window.scene = post_scene
bpy.ops.object.select_all(action='DESELECT')
post_obj.select_set(True)
bpy.context.view_layer.objects.active = post_obj
# Set output image
post_out_col.image = post_col
post_out_col.select = True
post_mat_nodes.active = post_out_col
output_switch.default_value = 0.0
mask_switch.default_value = 0.0
use_mask.default_value = 0.0
if masked:
use_mask.default_value = 1.0
# Generate output
bpy.context.view_layer.update()
_print("> -Performing post bake processing", tag=True, wrap=False)
err = False
try:
bpy.ops.object.bake(
type="EMIT",
save_mode='INTERNAL',
use_clear=False,
)
except RuntimeError as error:
_print(": %s" % (error), tag=True)
err = True
else:
post_out_col.select = False
if post_alp is None and post_msk is None:
_print("", tag=True)
# Set alpha output if enabled
if post_alp:
post_out_alp.image = post_alp
post_out_alp.select = True
post_mat_nodes.active = post_out_alp
output_switch.default_value = 1.0
mask_switch.default_value = 0.0
# Generate alpha output
bpy.context.view_layer.update()
try:
bpy.ops.object.bake(
type="EMIT",
save_mode='INTERNAL',
use_clear=False,
)
except RuntimeError as error:
_print(": %s" % (error), tag=True)
err = True
else:
post_out_alp.select = False
if post_msk is None:
_print("", tag=True)
# Set mask output if enabled
if post_msk:
post_out_msk.image = post_msk
post_out_msk.select = True
post_mat_nodes.active = post_out_msk
output_switch.default_value = 0.0
mask_switch.default_value = 1.0
# Generate mask output
bpy.context.view_layer.update()
try:
bpy.ops.object.bake(
type="EMIT",
save_mode='INTERNAL',
use_clear=False,
)
except RuntimeError as error:
_print(": %s" % (error), tag=True)
err = True
else:
post_out_msk.select = False
_print("", tag=True)
return err
# Combine alpha channel with color image
def alpha_pass(color, alpha, combined):
stride = 4
col_px = list(color.pixels)
alp_px = list(alpha.pixels)
# Sanity check
if len(col_px) != len(alp_px):
_print(": Input/Output pixel count mismatch", tag=True)
return True
# Write channel
for pixel in range(int(len(col_px)/stride)):
position = pixel * stride
alpha_ch = position + 3
col_px[alpha_ch] = alp_px[position] # Alpha image is greyscale, so any channel value will do
combined.pixels = col_px[:]
combined.update()
return False
# Paint a margin around masked objects to the specified pixels or completely fill the empty space
def paint_margin(image, mask, margin, fill, step=12, samples=3):
if fill: margin = -1
if not margin: return
start = datetime.now()
_print("> -Painting margin: ", tag=True, wrap=False)
# Put this in a try block because it can explode
try:
mpixels, mmask, mbools, mmargins, mw, mh, mmargin_step = marginer.set_up(image, mask, step)
margined = marginer.add_margin(mpixels, mmask, mbools, mmargins, mw, mh, mmargin_step, margin, samples)
marginer.write_back(image, margined)
except Exception as err:
_print("Failed (%s)" % (str(err)), tag=True)
else:
_print("Completed in %s" % (str(datetime.now() - start)), tag=True)
# Move a UDIM tile into the 0-1 UV range
def focus_udim_tile(uvmap, udim, unfocus=False):
# Calculate offset
v_shift = int((udim - 1001) / 10)
u_shift = int((udim - 1001) - (v_shift * 10))
if unfocus:
v_shift = v_shift * -1
u_shift = u_shift * -1
# Move all the UVs by offset
for uv in uvmap.data.values():
uv.uv[0] -= u_shift
uv.uv[1] -= v_shift
# Take a UV map and split it into UDIM tiles using standard format
def uv_to_udim(solution, key, uvmap):
tiles = solution.baketile[key]
udim = []
# TODO: Check fastest way to get all the uvs
uvs = [uvmap.data[i].uv[:] for i in range(len(uvmap.data.values()))]
for u, v in uvs:
if int(u) == u and u > 0: u -= 1
if int(v) == v and v > 0: v -= 1
tile_no = (int(v) * 10) + int(u) + 1001
if tile_no not in tiles:
tiles[tile_no] = []
if tile_no not in udim:
udim.append(tile_no)
udim.sort()
return udim
# Fast AA pass by scaling pixels
def fast_aa(image, level):
img_x = image.size[0]
img_y = image.size[1]
x_lvl = img_x * ((10 - level) * 0.1)
y_lvl = img_y * ((10 - level) * 0.1)
image.scale(int(x_lvl), int(y_lvl))
image.update()
image.scale(int(img_x), int(img_y))
image.update()
# Copy vert colors between object copies
def copy_vert_cols(name=None, cpy_from=None, cpy_to=None):
# Set array to size of data and copy it in
from_cols = cpy_from.data.color_attributes[name]
data = [0.0] * (len(from_cols.data) * 4)
from_cols.data.foreach_get('color', data)
# Create new color attrib and copy array into it
cpy_to.data.color_attributes.new(name, from_cols.data_type, from_cols.domain)
to_data = cpy_to.data.color_attributes[name].data
to_data.foreach_set('color', data)
# Add masks together by editing internal node group
def add_masks(masks, nodes, links):
mask_sock = nodes['AlphaSwitch'].inputs['Mask']
# Simplest case is one mask
prev_adder = None
if len(masks) == 1:
links.new(masks[0].outputs['Mask'], mask_sock)
# At least two masks or more
elif len(masks) > 1:
prev_adder = masks[0]
for idx in range(1, len(masks)):
# Add a mask adder node group
mask_adder = nodes.new('ShaderNodeGroup')
mask_adder.node_tree = internal_add_mask
# Link prev adder to new adder and mask to adder
links.new(prev_adder.outputs['Mask'], mask_adder.inputs['Mask1'])
links.new(masks[idx].outputs['Mask'], mask_adder.inputs['Mask2'])
prev_adder = mask_adder
# Link the last adder to mask socket
if prev_adder:
links.new(prev_adder.outputs['Mask'], mask_sock)
# Clear an existing image to all black all transparent
def clear_image(solution):
# Proceed if clear is set
if solution.node.img_clear:
output_path = solution.node.img_path
output_name = solution.node.name_with_ext()
output_file = os.path.join(os.path.realpath(output_path), output_name)
# Nothing to do if image doesn't exist
if os.path.exists(output_file):
img = bpy.data.images.load(os.path.abspath(output_file))
img.generated_type = 'BLANK'
img.generated_color = (0.0, 0.0, 0.0, 0.0)
img.generated_width = img.size[0]
img.generated_height = img.size[1]
img.source = 'GENERATED'
img.filepath_raw = output_file
img.save()
_print("> -Image Cleared", tag=True)
# Take a list of values and return the highest
def pixel_value(channels):
value = 0
for val in channels:
if val > value:
value = val
return value
# Apply image format settings to scenes output settings
def apply_output_format(target_settings, format):
# Configure output image settings
target_settings.file_format = img_type = format["img_type"]
# Color mode
target_settings.color_mode = format['img_color_mode']
# Color Depth
if format['img_color_depth']:
target_settings.color_depth = format['img_color_depth']
# Compression / Quality for formats that support it
if img_type == 'PNG':
target_settings.compression = format['img_quality']
elif img_type in ['JPEG', 'JPEG2000']:
target_settings.quality = format['img_quality']
# Codecs for formats that use them
if img_type == 'JPEG2000':
target_settings.jpeg2k_codec = format['img_codec']
elif img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']:
target_settings.exr_codec = format['img_codec']
elif img_type == 'TIFF':
target_settings.tiff_codec = format['img_codec']
# Additional settings used by some formats
if img_type == 'JPEG2000':
target_settings.use_jpeg2k_cinema_preset = format["img_jpeg2k_cinema"]
target_settings.use_jpeg2k_cinema_48 = format["img_jpeg2k_cinema48"]
target_settings.use_jpeg2k_ycc = format["img_jpeg2k_ycc"]
elif img_type == 'DPX':
target_settings.use_cineon_log = format["img_dpx_log"]
elif img_type == 'OPEN_EXR':
target_settings.use_zbuffer = format["img_openexr_zbuff"]
# Copy render settings from source scene to active scene
def copy_render_settings(source, target):
# Copy all Cycles settings
for setting in source.cycles.bl_rna.properties.keys():
if setting not in ["rna_type", "name"]:
setattr(target.cycles, setting, getattr(source.cycles, setting))
for setting in source.cycles_curves.bl_rna.properties.keys():
if setting not in ["rna_type", "name"]:
setattr(target.cycles_curves, setting, getattr(source.cycles_curves, setting))
# Copy SOME Render settings
for setting in source.render.bl_rna.properties.keys():
if setting in ["dither_intensity",
"filter_size",
"film_transparent",
"use_freestyle",
"threads",
"threads_mode",
"hair_type",
"hair_subdiv",
"use_simplify",
"simplify_subdivision",
"simplify_child_particles",
"simplify_subdivision_render",
"simplify_child_particles_render",
"use_simplify_smoke_highres",
"simplify_gpencil",
"simplify_gpencil_onplay",
"simplify_gpencil_view_fill",
"simplify_gpencil_remove_lines",
"simplify_gpencil_view_modifier",
"simplify_gpencil_shader_fx",
"simplify_gpencil_blend",
"simplify_gpencil_tint",
]:
setattr(target.render, setting, getattr(source.render, setting))
# Pretty much everything here is about preventing blender crashing or failing in some way that only happens
# when it runs a background bake. Perhaps it wont be needed some day, but for now trying to keep all such
# things in one place. Modifiers are applied or removed and non mesh types are converted.
def prep_object_for_bake(object, strip=False, invert=False, vcols=[False, None]):
# Create a copy of the object to modify and put it into the mesh only scene
if ((not object.bw_copy or object.bw_copy_frame != current_frame) and not strip and not vcols[0])\
or ((not object.bw_strip or object.bw_strip_frame != current_frame) and strip and not vcols[0])\
or (not object.bw_vcols and vcols[0]) or user_prop:
copy = object.copy()
copy.bw_copy = None
copy.bw_strip = None
copy.bw_vcols = None
copy.data = object.data.copy()
bw_solution_data['objects'].append(copy)
copy.name = ("BW_SMOD_" if strip else ("BW_VCOL_" if vcols[0] else "BW_")) + object.name
base_scene.collection.objects.link(copy)
bpy.context.view_layer.update()
else:
# Object already preped
retcpy = (object.bw_strip if strip else (object.bw_vcols if vcols[0] else object.bw_copy))
ret = retcpy.copy()
ret.data = retcpy.data.copy()
return ret
# Can't modify object for vertex colors as vert count might change
if vcols[0] and object == vcols[1] and not strip:
object.bw_vcols = copy
bw_solution_data['objects'].append(copy)
mesh_scene.collection.objects.link(copy)
base_scene.collection.objects.unlink(copy)
# Create copy of the copy for return object
ret = copy.copy()
ret.data = copy.data.copy()
return ret
# Objects need to be selectable and visible in the viewport in order to convert them
copy.hide_select = False
copy.hide_viewport = False
# If ignoring visibility is enabled, also make the object shown for render
if ignorevis:
copy.hide_render = False
# Apply active shape keys to copy so that modifiers can be applied
if hasattr(copy.data, "shape_keys") and copy.data.shape_keys is not None:
copy.shape_key_add(name="BW_Combined", from_mix=True)
for skey in copy.data.shape_keys.key_blocks:
copy.shape_key_remove(skey)
# Make obj the only selected + active
bpy.ops.object.select_all(action='DESELECT')
copy.select_set(True)
bpy.context.view_layer.objects.active = copy
# Deal with mods
if len(copy.modifiers):
for mod in copy.modifiers:
show_vp = mod.show_viewport
if invert: show_vp = not show_vp
if mod.show_render and (not strip or not show_vp):
# A mod can be disabled by invalid settings, which will throw an exception when trying to apply it
try:
if object.type == 'MESH':
bpy.ops.object.make_local()
bpy.ops.object.modifier_apply(modifier=mod.name)
except:
_print("> Error applying modifier '%s' to object '%s'" % (mod.name, object.name), tag=True)
bpy.ops.object.modifier_remove(modifier=mod.name)
else:
bpy.ops.object.modifier_remove(modifier=mod.name)
# Deal with object type
if object.type != 'MESH':
# Apply render resolution if its set before turning into a mesh
if object.type == 'META':
if copy.data.render_resolution > 0:
copy.data.resolution = copy.data.render_resolution
else:
if copy.data.render_resolution_u > 0:
copy.data.resolution_u = copy.data.render_resolution_u
if object.data.render_resolution_v > 0:
copy.data.resolution_v = copy.data.render_resolution_v
# Convert
bpy.ops.object.convert(target='MESH')
# Meta objects seem to get deleted and a new object replaces them, breaking the reference
if object.type == 'META':
copy = bpy.context.view_layer.objects.active
# Link copy to original, remove from base scene and add to mesh scene
if strip:
object.bw_strip = copy
object.bw_strip_frame = current_frame
else:
object.bw_copy = copy
object.bw_copy_frame = current_frame
mesh_scene.collection.objects.link(copy)
base_scene.collection.objects.unlink(copy)
# Return copy of copy
ret = copy.copy()
ret.data = copy.data.copy()
return ret
# Takes a materials node tree and makes any changes necessary to perform the given bake type. A material must
# end with principled shader(s) and mix shader(s) connected to a material output in order to be set up for any
# emission node bakes.
def prep_material_for_bake(node_tree, bake_type, bake_settings):
# Bake types with built-in passes don't require any preparation
if not node_tree or (bake_settings['bake_cat'] != 'PBR' and bake_type not in ['MASK', 'OSL_BENTNORM']):
return
# Mask is a special case where an emit shader and output can just be added to any material
elif bake_type == 'MASK':
nodes = node_tree.nodes
# Add white emit and a new active output
emit = nodes.new('ShaderNodeEmission')
emit.inputs['Color'].default_value = [1.0, 1.0, 1.0, 1.0]
outp = nodes.new('ShaderNodeOutputMaterial')
node_tree.links.new(emit.outputs['Emission'], outp.inputs['Surface'])
outp.target = 'CYCLES'
# Make all outputs not active
for node in nodes:
if node.type == 'OUTPUT_MATERIAL':
node.is_active_output = False
outp.is_active_output = True
return
# The material has to have a node tree and it needs at least 2 nodes to be valid
elif len(node_tree.nodes) < 2:
# AOV bake only requires that material has the named AOV node
if bake_type == 'AOV' and len(node_tree.nodes):
pass
else:
return
# All other bake types use an emission shader with the value plugged into it
# A material can have multiple output nodes. Blender seems to preference the output to use like so:
# 1 - Target set to current Renderer and Active (picks first if multiple are set active)
# 2 - First output with Target set to Renderer if no others with that target are set Active
# 3 - Active output (picks first if mutliple are active)
#
# Strategy will be to find all valid outputs and evaluate if they can be used in the same order as above.
# The first usable output found will be selected and also changed to be the first choice for blender.
# Four buckets: Cycles + Active, Cycles, Generic + Active, Generic
nodes = node_tree.nodes
node_cycles_out_active = []
node_cycles_out = []
node_generic_out_active = []
node_generic_out = []
node_selected_output = None
# Collect all outputs
for node in nodes:
if node.type == 'OUTPUT_MATERIAL':
if node.target == 'CYCLES':
if node.is_active_output:
node_cycles_out_active.append(node)
else:
node_cycles_out.append(node)
elif node.target == 'ALL':
if node.is_active_output:
node_generic_out_active.append(node)
else:
node_generic_out.append(node)
# Select the first usable output using the order explained above and make sure no other outputs are set active
node_outputs = node_cycles_out_active + node_cycles_out + node_generic_out_active + node_generic_out
for node in node_outputs:
input = node.inputs['Surface']
if not node_selected_output: # and material_recursor(node):
node_selected_output = node
node.is_active_output = True
else:
node.is_active_output = False
if not node_selected_output:
# For an AOV bake, just add an output now if there wasn't one
if bake_type == 'AOV':
node_selected_output = nodes.new('ShaderNodeOutputMaterial')
node_selected_output.is_active_output = True
else:
return
# Output has been selected. An emission shader will now be built, replacing mix shaders with mix RGB
# nodes and principled shaders with just the desired data for the bake type. Recursion used.
if debug: _print("> Chosen output [%s] descending tree:" % (node_selected_output.name), tag=True)
class linkcls():
def __init__(self):
self.link_data = None
self.link_list = []
def set_links(self, link_data): self.link_data = link_data
def new(self, lto, lfrom): self.link_list.append([self.link_data, lto, lfrom])
def remove(self, link): self.link_data.remove(link)
def create(self):
for link in self.link_list:
if debug: _print("> Linking: %s to %s" % (link[1].node.name, link[2].node.name), tag=True)
link[0].new(link[1], link[2])
link_list = linkcls()
ret = prep_material_rec(node_selected_output, None, node_selected_output.inputs['Surface'], bake_type, bake_settings, link_list)
if bake_type != 'OSL_BENTNORM':
link_list.create()
return ret
# Takes a node of type OUTPUT_MATERIAL, BSDF_PRINCIPLED or MIX/ADD_SHADER. Starting with an output node it will
# recursively generate an emission shader to replace the output with the desired bake type. The link_socket
# is used for creating node tree links. Also added dealing with Custom Node Groups.
def prep_material_rec(node, socket, link_socket, bake_type, bake_settings, link_list, parent=None):
if debug: print("%s:%s->%s:%s" % (link_socket.node.type, link_socket.name, node.name, (socket.name if socket else "None")))
tree = node.id_data
nodes = tree.nodes
links = link_list
links.set_links(tree.links)
# Helper to get links or values for node inputs
def link_or_value(socket):
if socket.is_linked: return follow_input_link(socket.links[0]).from_socket, 0
else: return socket.default_value, 1
# Helper to either create a link or set the default value
def map_input_or_value(proxy, proxy_input, socket):
sock, is_val = link_or_value(socket)
if is_val: proxy.inputs[proxy_input].default_value = sock
else: links.link_data.new(sock, proxy.inputs[proxy_input])
# Helper to check both inputs to mix/add are connected
def check_both_inputs(node):
if node.type == 'MIX_SHADER':
input1 = 1
input2 = 2
else:
input1 = 0
input2 = 1
recon_link = 0
if node.inputs[input1].is_linked:
recon_link = 1
elif node.inputs[input2].is_linked:
recon_link = 2
return [(node.inputs[input1].is_linked) and (node.inputs[input2].is_linked), recon_link]
# Cases:
if node.type == 'OUTPUT_MATERIAL':
# Start of shader. Create new emission shader and connect it to the output
next = follow_input_link(link_socket.links[0])
next_node = next.from_node
next_sock = next.from_socket
node_emit = nodes.new('ShaderNodeEmission')
if bake_settings['bbbk']:
# billboard bake needs to respect alpha by creating a separate alpha mix
bbbk_node = nodes.new('ShaderNodeBsdfTransparent')
bbbk_mix = nodes.new('ShaderNodeMixShader')
bbbk_mix.inputs['Fac'].default_value = 0.0
bake_settings['bbbk_sock'] = bbbk_mix.inputs['Fac']
# Link it all up
links.new(node_emit.outputs['Emission'], bbbk_mix.inputs[2])
links.new(bbbk_node.outputs['BSDF'], bbbk_mix.inputs[1])
sock_toout = bbbk_mix.outputs['Shader']
else:
sock_toout = node_emit.outputs['Emission']
# Link output
links.new(sock_toout, link_socket)
# AOV bake will try to find and connect the named AOV node to the shader now and return
if bake_type == 'AOV':
# Helper to find named AOV
def find_aov(named, nodes):
first = not len(named)
for node in nodes:
if node.type == 'OUTPUT_AOV':
if first or node.name == named:
return node
return None
aov_node = find_aov(bake_settings['aov_name'], nodes)
if aov_node:
if bake_settings['aov_input'] == 'COL':
map_input_or_value(node_emit, 'Color', aov_node.inputs['Color'])
else:
socket, is_val = link_or_value(aov_node.inputs['Value'])
if is_val: node_emit.inputs['Color'].default_value = [socket, socket, socket, socket]
else: links.new(socket, node_emit.inputs['Color'])
return True
else:
# Set it to black if not found?
node_emit.inputs['Color'].default_value = [0,0,0,0]
return False
else:
# Recurse
ret = prep_material_rec(next_node, next_sock, node_emit.inputs['Color'], bake_type, bake_settings, links, parent)
return ret
if node.type in ['MIX_SHADER', 'ADD_SHADER']:
# If there aren't two inputs then the node is essentially muted, so a delete with reconnect should be the answer..
check_inputs = check_both_inputs(node)
'''if not check_inputs[0]:
if check_inputs[1] is not None:
return prep_material_rec(check_inputs[1].from_node, check_inputs[1].from_socket, link_socket, bake_type, bake_settings, links, parent)
else: return False'''
# Mix shader needs to generate a mix RGB maintaining the same Fac input if linked
mix_node = nodes.new('ShaderNodeMixRGB')
mix_node.label = node.label
mix_node.location = node.location
mix_node.mute = node.mute
mix_node.blend_type = 'MIX'
if bake_settings['bbbk']: # duplicate everything for alpha
bbmix_node = nodes.new('ShaderNodeMixRGB')
bbmix_node.blend_type = 'MIX'
if node.type == 'MIX_SHADER' and node.inputs['Fac'].is_linked:
# Connect Fac input
links.new(follow_input_link(node.inputs['Fac'].links[0]).from_socket, mix_node.inputs['Fac'])
if bake_settings['bbbk']: links.new(follow_input_link(node.inputs['Fac'].links[0]).from_socket, bbmix_node.inputs['Fac'])
else:
if node.type == 'MIX_SHADER':
# Set Fac value to match instead
mix_node.inputs['Fac'].default_value = node.inputs['Fac'].default_value
if bake_settings['bbbk']: bbmix_node.inputs['Fac'].default_value = node.inputs['Fac'].default_value
else:
# Add shader is a Fac of 1
mix_node.inputs['Fac'].default_value = 1
mix_node.blend_type = 'ADD'
if bake_settings['bbbk']:
bbmix_node.inputs['Fac'].default_value = 1
bbmix_node.blend_type = 'ADD'
# Connect mix output to previous socket
links.new(mix_node.outputs['Color'], link_socket)
if bake_settings['bbbk']: links.new(bbmix_node.outputs['Color'], bake_settings['bbbk_sock'])
# Recurse
if node.type == 'MIX_SHADER':
input1 = 1
input2 = 2
else:
input1 = 0
input2 = 1
if not check_inputs[0]:
if check_inputs[1] == 1:
nextA = follow_input_link(node.inputs[input1].links[0])
nextB = False
elif check_inputs[1] == 2:
nextA = False
nextB = follow_input_link(node.inputs[input2].links[0])
else:
return False
else:
nextA = follow_input_link(node.inputs[input1].links[0])
nextB = follow_input_link(node.inputs[input2].links[0])
if nextA:
if bake_settings['bbbk']: bake_settings['bbbk_sock'] = bbmix_node.inputs['Color1']
branchA = prep_material_rec(nextA.from_node, nextA.from_socket, mix_node.inputs['Color1'], bake_type, bake_settings, links, parent)
else:
mix_node.inputs['Color1'].default_value = [0,0,0,0]
branchA = True
if nextB:
if bake_settings['bbbk']: bake_settings['bbbk_sock'] = bbmix_node.inputs['Color2']
branchB = prep_material_rec(nextB.from_node, nextB.from_socket, mix_node.inputs['Color2'], bake_type, bake_settings, links, parent)
else:
mix_node.inputs['Color2'].default_value = [0,0,0,0]
branchB = True
return branchA and branchB
if node.type == 'GROUP' and socket:
# First the 'group' should be replaced with a copy along with adding a RGBA output and connecting it
node.node_tree = node.node_tree.copy()
if bake_settings['bbbk']:
node.node_tree.outputs.new('NodeSocketColor', 'BW_BBOut')
links.new(bake_settings['bbbk_sock'], node.outputs[-1])
node.node_tree.outputs.new('NodeSocketColor', 'BW_Out')
links.new(link_socket, node.outputs[-1])
# Set links to the internal tree
links.set_links(node.node_tree.links)
# Entering a custom group, has some code duplication with exiting
# Descending node tree, origin needs to be added to parent stack (using a copy to deal with branching)
if parent:
gparent = parent.copy()
gparent.append(node)
else:
gparent = [node]
gout = None
gsoc = 0
# Get the index of the socket as names may not be unique then find internal socket
for soc in node.outputs:
if socket == soc: break
else: gsoc += 1
for gnode in node.node_tree.nodes:
if gnode.type == 'GROUP_OUTPUT' and gnode.is_active_output:
gout = gnode
break
# Quick sanity check
if not gout or not gout.inputs[gsoc].is_linked:
if debug: _print("> Error: Group node active output socket not linked", tag=True)
return True
# Follow the next node to be considered, use the added RGBA socket as link_sock (should be 2nd last)
next = follow_input_link(gout.inputs[gsoc].links[0])
if bake_settings['bbbk']: bake_settings['bbbk_sock'] = gout.inputs[-3]
return prep_material_rec(next.from_node, next.from_socket, gout.inputs[-2], bake_type, bake_settings, links, gparent)
if node.type == 'GROUP_INPUT' and socket and parent:
# Exiting a custom group, has some code duplication with entering
# Acceding node tree, pop origin off the parent stack (using a copy to deal with branching)
gparent = parent.copy()
gout = gparent.pop()
gsoc = 0
# First add a new RGBA input to the parent and connect it to the previous thing (should be 2nd last on inside)
if bake_settings['bbbk']:
gout.node_tree.inputs.new('NodeSocketColor', 'BW_BBIn')
links.new(bake_settings['bbbk_sock'], node.outputs[-2])
gout.node_tree.inputs.new('NodeSocketColor', 'BW_In')
links.new(link_socket, node.outputs[-2])
# Set links to the external tree
links.set_links(gout.id_data.links)
# Get index of the socket as names may not be unique
for soc in node.outputs:
if socket == soc: break
else: gsoc += 1
# Quick sanity check
if not gout or not gout.inputs[gsoc].is_linked:
if debug: _print("> Error: Group node input socket not linked", tag=True)
return True
# Return the next node to be considered, using the added RGBA external socket as the link_sock (should be last)
next = follow_input_link(gout.inputs[gsoc].links[0])
if bake_settings['bbbk']: bake_settings['bbbk_sock'] = gout.inputs[-2]
return prep_material_rec(next.from_node, next.from_socket, gout.inputs[-1], bake_type, bake_settings, links, gparent)
# All of the shader types that aren't really supported. Instead of failing, they will be converted to Principled using some assumptions.
if node.type in ['BSDF_DIFFUSE', 'BSDF_GLOSSY', 'BSDF_GLASS', 'BSDF_REFRACTION', 'BSDF_TRANSLUCENT', 'BSDF_TRANSPARENT', 'BSDF_ANISOTROPIC', 'SUBSURFACE_SCATTERING', 'EMISSION', 'HOLDOUT', 'BSDF_HAIR', 'BSDF_HAIR_PRINCIPLED', 'PRINCIPLED_VOLUME', 'BSDF_TOON', 'BSDF_VELVET', 'VOLUME_ABSORPTION', 'VOLUME_SCATTER']:
proxy = nodes.new('ShaderNodeBsdfPrincipled')
proxy.inputs['Base Color'].default_value = [0,0,0,0]
#links.new(proxy.outputs[0], link_socket)
# Almost all of these have a color input that can be mapped
if node.type != 'HOLDOUT':
if node.type == 'SUBSURFACE_SCATTERING':
map_input_or_value(proxy, 'Subsurface Color', node.inputs['Color'])
elif node.type == 'EMISSION':
map_input_or_value(proxy, 'Emission', node.inputs['Color'])
else:
map_input_or_value(proxy, 'Base Color', node.inputs['Color'])
# Many also have a roughness value
if node.type in ['BSDF_GLOSSY', 'BSDF_DIFFUSE', 'BSDF_GLASS', 'BSDF_REFRACTION', 'BSDF_ANISOTROPIC', 'BSDF_HAIR_PRINCIPLED']:
map_input_or_value(proxy, 'Roughness', node.inputs['Roughness'])
# A bunch can have a normal input mapped
if node.type in ['BSDF_GLOSSY', 'BSDF_DIFFUSE', 'BSDF_GLASS', 'BSDF_REFRACTION', 'SUBSURFACE_SCATTERING', 'BSDF_ANISOTROPIC', 'BSDF_TRANSLUCENT', 'BSDF_TOON', 'BSDF_VELVET']:
map_input_or_value(proxy, 'Normal', node.inputs['Normal'])
# Not really any more common properties, but still a few that can be mapped
if node.type in ['BSDF_GLASS', 'BSDF_REFRACTION', 'BSDF_HAIR_PRINCIPLED']:
map_input_or_value(proxy, 'IOR', node.inputs['IOR'])
if node.type == 'EMISSION':
map_input_or_value(proxy, 'Emission Strength', node.inputs['Strength'])
if node.type == 'SUBSURFACE_SCATTERING':
map_input_or_value(proxy, 'Subsurface Radius', node.inputs['Radius'])
if node.type in ['BSDF_ANISOTROPIC', 'PRINCIPLED_VOLUME', 'VOLUME_SCATTER']:
map_input_or_value(proxy, 'Anisotropic', node.inputs['Anisotropy'])
if node.type == 'BSDF_ANISOTROPIC':
map_input_or_value(proxy, 'Anisotropic Rotation', node.inputs['Rotation'])
if node.type in ['BSDF_HAIR', 'BSDF_ANISOTROPIC']:
map_input_or_value(proxy, 'Tangent', node.inputs['Tangent'])
# Work out a metalness based on some nodes
if node.type in ['BSDF_DIFFUSE', 'BSDF_GLOSSY', 'BSDF_TOON', 'BSDF_ANISOTROPIC']:
if node.type == 'BSDF_TOON':
if node.component == 'DIFFUSE': proxy.inputs['Metallic'].default_value = 0.0
else: proxy.inputs['Metallic'].default_value = 1.0
elif node.type == 'BSDF_DIFFUSE': proxy.inputs['Metallic'].default_value = 0.0
elif node.type == 'BSDF_GLOSSY': proxy.inputs['Metallic'].default_value = 1.0
elif node.type == 'BSDF_ANISOTROPIC': proxy.inputs['Metallic'].default_value = 1.0
# Clear coat
if node.type == 'BSDF_HAIR_PRINCIPLED':
map_input_or_value(proxy, 'Clearcoat', node.inputs['Coat'])
if node.type == 'PRINCIPLED_VOLUME':
map_input_or_value(proxy, 'Emission', node.inputs['Emission Color'])
map_input_or_value(proxy, 'Emission Strength', node.inputs['Emission Strength'])
# Alpha
if node.type == 'BSDF_TRANSPARENT':
proxy.inputs['Alpha'].default_value = 0.0
return prep_material_rec(proxy, proxy.outputs[0], link_socket, bake_type, bake_settings, links, parent)
if node.type == 'BSDF_PRINCIPLED':
# End of a branch as far as the prep is concerned. Either link the desired bake value or set the
# previous socket to the value if it isn't linked
if bake_type == 'ALBEDO':
bake_input = node.inputs['Base Color']
elif bake_type == 'SUBSURFACE':
bake_input = node.inputs['Subsurface']
elif bake_type == 'SUBRADIUS':
bake_input = node.inputs['Subsurface Radius']
elif bake_type == 'SUBCOLOR':
bake_input = node.inputs['Subsurface Color']
elif bake_type == 'METALLIC':
bake_input = node.inputs['Metallic']
elif bake_type == 'SPECULAR':
bake_input = node.inputs['Specular']
elif bake_type == 'SPECTINT':
bake_input = node.inputs['Specular Tint']
elif bake_type in ['ROUGHNESS' ,'SMOOTHNESS']:
bake_input = node.inputs['Roughness']
elif bake_type == 'ANISOTROPIC':
bake_input = node.inputs['Anisotropic']
elif bake_type == 'ANISOROTATION':
bake_input = node.inputs['Anisotropic Rotation']
elif bake_type == 'SHEEN':
bake_input = node.inputs['Sheen']
elif bake_type == 'SHEENTINT':
bake_input = node.inputs['Sheen Tint']
elif bake_type == 'CLEARCOAT':
bake_input = node.inputs['Clearcoat']
elif bake_type == 'CLEARROUGH':
bake_input = node.inputs['Clearcoat Roughness']
elif bake_type == 'TRANSIOR':
bake_input = node.inputs['IOR']
elif bake_type == 'TRANSMISSION':
bake_input = node.inputs['Transmission']
elif bake_type == 'TRANSROUGH':
bake_input = node.inputs['Transmission Roughness']
elif bake_type == 'EMIT':
bake_input = node.inputs['Emission']
elif bake_type == 'ALPHA':
bake_input = node.inputs['Alpha']
elif bake_type in ['TEXNORM', 'OBJNORM', 'BBNORM', 'OSL_BENTNORM']:
bake_input = node.inputs['Normal']
elif bake_type == 'CLEARNORM':
bake_input = node.inputs['Clearcoat Normal']
else:
bake_input = None
if debug: _print("> Reached branch end, ", tag=True, wrap=False)
if bake_input:
if bake_settings['bbbk']:
# Connect alpha to billboard bake alpha mix nodes
bbbk_input = node.inputs['Alpha']
if bbbk_input.is_linked:
links.new(bbbk_input.links[0].from_socket, bake_settings['bbbk_sock'])
else:
# Create a color node to use as input
bbcolornode = nodes.new('ShaderNodeRGB')
bbcolorsoct = bbcolornode.outputs["Color"]
links.new(bake_settings['bbbk_sock'], bbcolorsoct)
bbcolorsoct.default_value[0] = bbbk_input.default_value
bbcolorsoct.default_value[1] = bbbk_input.default_value
bbcolorsoct.default_value[2] = bbbk_input.default_value
bbcolorsoct.default_value[3] = 1.0
if bake_type in ['TEXNORM', 'CLEARNORM', 'BBNORM']:
# Normal map types need an extra step, plus configuration of space and swizzle
# Add normal mapping node group
normgrp = nodes.new('ShaderNodeGroup')
if bake_type == 'BBNORM':
normgrp.node_tree = billboard_norm.copy()
normgrp.inputs['Rotation'].default_value = bake_settings['bb_rot']
else:
normgrp.node_tree = normals_group.copy()
normnod = normgrp.node_tree.nodes
normlnk = normgrp.node_tree.links
# Link it
links.new(normgrp.outputs["Color"], link_socket)
link_socket = normgrp.inputs["Normal"]
# Configure it
if bake_type == 'BBNORM':
spacenod = normnod["bw_norm"]
else:
normlnk.new(normnod["bw_inputnorm"].outputs["Normal"], normnod["bw_normal_input"].inputs[0])
spacenod = normnod[bake_settings["norm_s"]]
swizzleR = spacenod.outputs[bake_settings["norm_r"]]
swizzleG = spacenod.outputs[bake_settings["norm_g"]]
swizzleB = spacenod.outputs[bake_settings["norm_b"]]
normoutp = normnod["bw_normal_xyz"]
normlnk.new(swizzleR, normoutp.inputs["X"])
normlnk.new(swizzleG, normoutp.inputs["Y"])
normlnk.new(swizzleB, normoutp.inputs["Z"])
if bake_type in ['OSL_BENTNORM']:
# Add bent normals group to material and set values
bentnormgrp = nodes.new('ShaderNodeGroup')
bentnormgrp.node_tree = bent_norm_group
bentnormgrp.inputs['Distance'].default_value = bake_settings['osl_bentnorm_dist']
bentnormgrp.inputs['Samples'].default_value = bake_settings['osl_bentnorm_samp']
# Link it
if bake_input.is_linked:
if debug: _print("Normals Link found, [%s] will be connected" % (bake_input.links[0].from_socket.name), tag=True)
tree.links.new(bake_input.links[0].from_socket, bentnormgrp.inputs['Normal'])
else: # For Bent normals just connect object normals instead
if debug: _print("Nomrals not linked, objects normals used instead", tag=True)
objgeo = nodes.new('ShaderNodeNewGeometry')
tree.links.new(objgeo.outputs['Normal'], bentnormgrp.inputs['Normal'])
tree.links.new(bentnormgrp.outputs["Bent Normal"], bake_input)
return True
if bake_input.is_linked:
if debug: _print("Link found, [%s] will be connected" % (bake_input.links[0].from_socket.name), tag=True)
if bake_type == 'OBJNORM':
# Remove any texture influence to normals
links.remove(bake_input.links[0])
else:
# Connect the linked node up to the emit shader
links.new(bake_input.links[0].from_socket, link_socket)
else:
if debug: _print("Not linked, value will be copied", tag=True)
if link_socket.is_linked:
links.remove(link_socket.links[0])
# Copy the value into the socket instead
if bake_input.type == 'RGBA':
link_socket.default_value = bake_input.default_value
elif bake_input.type == 'VECTOR':
link_socket.default_value[0] = bake_input.default_value[0]
link_socket.default_value[1] = bake_input.default_value[1]
link_socket.default_value[2] = bake_input.default_value[2]
#link_socket.default_value[3] = 1.0
else:
# Create a color node to use as input
colornode = nodes.new('ShaderNodeRGB')
colorsoct = colornode.outputs["Color"]
links.new(link_socket, colorsoct)
colorsoct.default_value[0] = bake_input.default_value
colorsoct.default_value[1] = bake_input.default_value
colorsoct.default_value[2] = bake_input.default_value
colorsoct.default_value[3] = 1.0
# Branch completed
return True
# Something went wrong
if debug: _print("> Error: Reached unsupported node type", tag=True)
return False
# Make sure object has at least a generic material
def check_has_material(object, materials, mat):
# A material is generally required to direct baking output, so add a generic one if none is present
add_mat = True
if len(object.material_slots):
for slot in object.material_slots:
if slot.material:
add_mat = False
break
if add_mat:
if materials and mat.name not in materials:
materials[mat.name] = mat
object.data.materials.append(mat)
# Replace all materials in scene with a shader for bake type
def replace_materials_for_shader_bake(scene, bake_settings, mesh_settings):
bake_type = bake_settings['bake_type']
bake_cat = bake_settings['bake_cat']
override = mesh_settings['material_replace']
override_mat = mesh_settings['material_override']
for obj in scene.objects:
# First strip existing materials unless they are needed
if bake_type not in ['MATID', 'OSL_BENTNORM']:
obj.data.materials.clear()
obj.data.polygons.foreach_set('material_index', [0] * len(obj.data.polygons))
obj.data.update()
# Do material override if it makes sense
if bake_cat != 'WRANG' and override and override_mat:
obj.data.materials.append(override_mat)
# Add correct shader for bake type
elif bake_type == 'BEVMASK':
obj.data.materials.append(bevmask_shader)
elif bake_type == 'BEVNORMEMIT':
obj.data.materials.append(bevnormemit_shader)
elif bake_type == 'BEVNORMNORM':
obj.data.materials.append(bevnormnorm_shader)
elif bake_type == 'CAVITY':
obj.data.materials.append(cavity_shader)
elif bake_type == 'CURVATURE':
obj.data.materials.append(curvature_shader)
elif bake_type == 'ISLANDID':
obj.data.materials.append(islandid_shader)
elif bake_type == 'OBJCOL':
obj.data.materials.append(objcol_shader)
elif bake_type == 'WORLDPOS':
obj.data.materials.append(worldpos_shader)
elif bake_type == 'THICKNESS':
obj.data.materials.append(thickness_shader)
elif bake_type == 'VERTCOL':
obj.data.materials.append(vertcol_shader)
elif bake_type == 'OSL_CURV':
obj.data.materials.append(osl_curvature)
elif bake_type == 'MASKPASS':
obj.data.materials.append(mask_pass_shader)
# Consider all materials in scene and create scene only copies
def make_materials_unique_to_scene(scene, suffix, bake_settings):
# Go through all the materials on every object
materials = {}
bake_type = bake_settings['bake_type']
bake_cat = bake_settings['bake_cat']
for obj in scene.objects:
if len(obj.material_slots):
for slot in obj.material_slots:
if slot.material:
# If its a new material, create a copy (adding suffix) and add the pair to the list
if slot.material.name not in materials:
copy = slot.material.copy()
copy.name = slot.material.name + suffix
materials[slot.material.name] = copy
replace = copy
else:
replace = materials[slot.material.name]
# Replace with copy
slot.material = replace
# For shader bakes there should now be only one material and it will be configured now
if bake_cat == 'WRANG':
mat_key = materials.keys()
if len(mat_key) > 1 and bake_type != 'MATID': _print("> Sanity Error: Doing shader bake, but objects had multiple materials", tag=True)
for key in mat_key:
nodes = materials[key].node_tree.nodes
if bake_type in ['BEVMASK', 'BEVNORMEMIT', 'BEVNORMNORM']:
bevel = nodes["bw_bevel"]
bevel.inputs["Radius"].default_value = bake_settings["bev_rad"]
bevel.samples = bake_settings["bev_samp"]
if bake_type == 'BEVNORMEMIT':
# Configure normal settings
bevel_norm = nodes["BW_Normals.Bevel"]
normnod = bevel_norm.node_tree.nodes
normlnk = bevel_norm.node_tree.links
spacenod = normnod[bake_settings["norm_s"]]
swizzleR = spacenod.outputs[bake_settings["norm_r"]]
swizzleG = spacenod.outputs[bake_settings["norm_g"]]
swizzleB = spacenod.outputs[bake_settings["norm_b"]]
normoutp = normnod["bw_normal_xyz"]
normlnk.new(swizzleR, normoutp.inputs["X"])
normlnk.new(swizzleG, normoutp.inputs["Y"])
normlnk.new(swizzleB, normoutp.inputs["Z"])
elif bake_type in ['CAVITY', 'THICKNESS']:
node_ao = nodes["bw_ao"]
node_ao.inputs["Distance"].default_value = bake_settings["cavity_dist"]
node_ao.samples = bake_settings["cavity_samp"]
if bake_type == 'CAVITY':
node_gamma = nodes["bw_ao_cavity_gamma"]
node_ao.inside = bake_settings["cavity_edges"]
node_gamma.inputs["Gamma"].default_value = bake_settings["cavity_gamma"]
elif bake_type == 'CURVATURE':
con_vex = nodes["bw_convex_range"]
con_cav = nodes["bw_concave_range"]
midv = bake_settings["curv_mid"]
cavv = bake_settings["curv_cav"]
vexv = bake_settings["curv_vex"]
con_vex.color_ramp.elements[0].color = [midv, midv, midv, 1.0]
con_vex.color_ramp.elements[1].color = [vexv, vexv, vexv, 1.0]
con_vex.color_ramp.elements[1].position = bake_settings["curv_vex_max"]
con_cav.color_ramp.elements[1].color = [midv, midv, midv, 1.0]
con_cav.color_ramp.elements[0].color = [cavv, cavv, cavv, 1.0]
con_cav.color_ramp.elements[0].position = bake_settings["curv_cav_min"]
elif bake_type == 'OSL_CURV':
osl_scr = nodes["bw_osl_script"]
osl_scr.inputs["Distance"].default_value = bake_settings["osl_curv_dist"]
osl_scr.inputs["Samples"].default_value = bake_settings["osl_curv_samp"]
osl_scr.inputs["contrast"].default_value = bake_settings["osl_curv_cont"]
if bake_settings["osl_curv_srgb"]:
osl_scr.inputs["srgb"].default_value = 1
else:
osl_scr.inputs["srgb"].default_value = 0
elif bake_type == 'OSL_HEIGHT':
osl_scr = nodes["bw_osl_script"]
osl_scr.inputs["Distance"].default_value = bake_settings["osl_height_dist"]
osl_scr.inputs["Midlevel"].default_value = bake_settings["osl_height_midl"]
osl_scr.inputs["iterations"].default_value = bake_settings["osl_height_samp"]
osl_scr.inputs["Voidcolor"].default_value = bake_settings["osl_height_void"]
elif bake_type == 'VERTCOL':
verts = nodes["bw_vertex_col"]
verts.layer_name = bake_settings["vert_col"]
elif bake_type == 'MATID':
# This one is different to the rest as it must replace all the existing materials with a unique per material color output
col_used = []
for name, mat in materials.items():
ntree = materials[name].node_tree
ntree.nodes.clear()
outp = ntree.nodes.new('ShaderNodeOutputMaterial')
if bake_settings["use_material_vpcolor"]:
# Use materials viewport color
colg = ntree.nodes.new('ShaderNodeEmission')
colg.inputs["Color"].default_value = materials[name].diffuse_color
else:
# Generate a random color using name as seed for repeatability
random.seed(a=name)
col = round(random.random(), 2)
while col in col_used:
round(random.random(), 2)
col_used.append(col)
# Clear all nodes and place the custom group connected to a output with the above value set
colg = ntree.nodes.new('ShaderNodeGroup')
colg.node_tree = materialid_group.copy()
colg.inputs["Random"].default_value = col
ntree.links.new(colg.outputs["Emission"], outp.inputs["Surface"])
# Return the dict
return materials
# Free up memory by removing created data
def free_data():
has_refs = []
if debug: _print("> Clearing data created for previous bake(s)", tag=True)
# Scenes first
while len(bw_solution_data['scenes']):
obj = bw_solution_data['scenes'].pop()
# Unlink any objects in the base collection
for obx in obj.collection.objects:
obj.collection.objects.unlink(obx)
# Remove the scene
if obj.name in bpy.data.scenes.keys(): bpy.data.scenes.remove(obj)
# Collections
while len(bw_solution_data['collections']):
obj = bw_solution_data['collections'].pop()
# Unlink any objects in the base collection
for obx in obj.objects:
obj.objects.unlink(obx)
# Remove collection
if obj.name in bpy.data.collections.keys() and not obj.users: bpy.data.collections.remove(obj)
else:
has_refs.append(['COL', obj])
if debug: _print("> Collection: %s removal postponed with %s users" % (obj.name, obj.users), tag=True)
# Objects
while len(bw_solution_data['objects']):
obj = bw_solution_data['objects'].pop()
# Unlink object from any collections
for col in bpy.data.collections:
if obj.name in col.objects.keys():
col.objects.unlink(obj)
# Unlink object from any scenes
for scn in bpy.data.scenes:
if obj.name in scn.collection.objects.keys():
scn.collection.objects.unlink(obj)
# Clear all materials from object
obj.data.materials.clear()
# Remove object
if obj.name in bpy.data.objects.keys() and not obj.users: bpy.data.objects.remove(obj)
else:
has_refs.append(['OBJ', obj])
if debug: _print("> Object: %s removal postponed with %s users" % (obj.name, obj.users), tag=True)
# Materials
while len(bw_solution_data['materials']):
obj = bw_solution_data['materials'].pop()
if obj.name in bpy.data.materials.keys() and not obj.users: bpy.data.materials.remove(obj)
else:
has_refs.append(['MAT', obj])
if debug: _print("> Material: %s removal postponed with %s users" % (obj.name, obj.users), tag=True)
# Images
while len(bw_solution_data['images']):
obj = bw_solution_data['images'].pop()
if obj.name in bpy.data.images.keys() and not obj.users: bpy.data.images.remove(obj)
else:
has_refs.append(['IMG', obj])
if debug: _print("> Image: %s removal postponed with %s users" % (obj.name, obj.users), tag=True)
# Data postponed due to having references last run
while len(bw_solution_data['had_refs']):
obj = bw_solution_data['had_refs'].pop()
if obj[0] == 'COL': dataBlk = bpy.data.collections
elif obj[0] == 'OBJ': dataBlk = bpy.data.objects
elif obj[0] == 'MAT': dataBlk = bpy.data.materials
elif obj[0] == 'IMG': dataBlk = bpy.data.images
else:
if debug: _print("> Unknown data type in postponed data blocks: %s for %s" % (obj[0], obj[1].name), tag=True)
continue
# Try to remove it again
if obj[1].name in dataBlk.keys() and not obj[1].users: dataBlk.remove(obj[1])
else:
has_refs.append([obj[0], obj[1]])
if debug: _print("> Postponed data block: %s removal postponed AGAIN with %s users" % (obj[1].name, obj[1].users), tag=True)
# Clear any zero ref count data blocks
#bpy.ops.outliner.orphans_purge(do_recursive=True)
# Add postponed data into data list
bw_solution_data['had_refs'] = has_refs
# Pack all textures
def pack_data():
if debug: _print("> Packing all textures into temporary blend file", tag=True)
for img in bpy.data.images:
if img.source != 'GENERATED' and not img.packed_file and len(img.packed_files.values()) == 0:
try:
img.pack()
except:
_print("> Unable to pack %s" % (img.name), tag=True)
# It's a me, main
def main():
import sys # to get command line args
import argparse # to parse options for us and print a nice help message
# get the args passed to blender after "--", all of which are ignored by
# blender so scripts may receive their own arguments
argv = sys.argv
if "--" not in argv:
argv = [] # as if no args are passed
else:
argv = argv[argv.index("--") + 1:] # get all args after "--"
# When --help or no args are given, print this help
usage_text = (
"This script is used internally by Bake Wrangler add-on."
)
parser = argparse.ArgumentParser(description=usage_text)
# Possible types are: string, int, long, choice, float and complex.
parser.add_argument(
"-t", "--tree", dest="tree", type=str, required=True,
help="Name of bakery tree where the starting node is",
)
parser.add_argument(
"-n", "--node", dest="node", type=str, required=True,
help="Name of bakery node to start process from",
)
parser.add_argument(
"-o", "--sock", dest="sock", type=int, required=False,
help="Socket for single suffix output",
)
parser.add_argument(
"-v", "--ignorevis", dest="ignorevis", type=int, required=False,
help="Treat all selected objects as visibile",
)
parser.add_argument(
"-d", "--debug", dest="debug", type=int, required=False,
help="Enable debug messages",
)
parser.add_argument(
"-r", "--rend_dev", dest="rend_dev", type=str, required=False,
help="Cycles render device type",
)
parser.add_argument(
"-u", "--rend_use", dest="rend_use", type=str, required=False,
help="Cycles enabled render devices",
)
parser.add_argument(
"--solitr", dest="solution_restart", type=str, required=False,
help="Iterations of solutions before retry",
)
parser.add_argument(
"--frameitr", dest="frames_restart", type=str, required=False,
help="Iterations of frames before retry",
)
parser.add_argument(
"--batchitr", dest="batch_restart", type=str, required=False,
help="Iterations of batches before retry",
)
args = parser.parse_args(argv)
if not argv:
parser.print_help()
return
if not args.tree or not args.node:
print("Error: Bake Wrangler baker required arguments not found")
return
global ignorevis
if args.ignorevis:
ignorevis = bool(args.ignorevis)
else:
ignorevis = False
global debug
if args.debug:
debug = bool(args.debug)
else:
debug = False
global images_saved
images_saved = []
global pickled_verts
pickled_verts = []
global solution_restart
solution_restart = 0
if args.solution_restart:
solution_restart = int(args.solution_restart)
global frames_restart
frames_restart = 0
if args.frames_restart:
frames_restart = int(args.frames_restart)
global batch_restart
batch_restart = 0
if args.batch_restart:
batch_restart = int(args.batch_restart)
# Track created data
global bw_solution_data
bw_solution_data = {'scenes': [],
'collections': [],
'objects': [],
'materials': [],
'images': [],
'had_refs': [],
}
# Reconfigure cycles render devices if args supplied
if args.rend_dev and args.rend_use:
bpy.context.preferences.addons["cycles"].preferences.get_devices()
bpy.context.preferences.addons["cycles"].preferences.compute_device_type = args.rend_dev
itr = 0
for char in args.rend_use:
bpy.context.preferences.addons["cycles"].preferences.devices[itr].use = int(char)
# Make sure the node classes are registered
try:
node_tree.register()
except:
print("Info: Bake Wrangler nodes already registered")
else:
print("Info: Bake Wrangler nodes registered")
# Make sure to be in object mode before doing anything
if bpy.context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
# Load shaders and scenes
bake_scene_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources", "BakeWrangler_Scene.blend")
with bpy.data.libraries.load(bake_scene_path, link=False, relative=False) as (file_from, file_to):
file_to.materials.append("BW_Bevel_Mask")
file_to.materials.append("BW_Bevel_Normals_Emit")
file_to.materials.append("BW_Bevel_Normals_Norm")
file_to.materials.append("BW_Cavity_Map")
file_to.materials.append("BW_Curvature_Map")
file_to.materials.append("BW_Island_ID")
file_to.materials.append("BW_Object_Color")
file_to.materials.append("BW_Thickness_Map")
file_to.materials.append("BW_Vertex_Color")
file_to.materials.append("BW_World_Pos")
file_to.materials.append("BW_OSL_Curvature")
file_to.materials.append("BW_OSL_Height")
file_to.materials.append("BW_Post_")
file_to.materials.append("BW_Post_Col_")
file_to.materials.append("BW_Billboard")
file_to.materials.append("BW_Mask_Pass")
file_to.node_groups.append("BW_Material_ID")
file_to.node_groups.append("BW_Normals")
file_to.node_groups.append("BW_Masked_Bake")
file_to.node_groups.append("BW_Channel_Map")
file_to.node_groups.append("bw_add_mask")
file_to.node_groups.append("BW_Billboard_Norm")
file_to.node_groups.append("BW_Bent_Norm")
file_to.scenes.append("BakeWrangler_Post")
file_to.scenes.append("BakeWrangler_Output")
file_to.objects.append("BW_MatPlane")
global bevmask_shader
global bevnormemit_shader
global bevnormnorm_shader
global cavity_shader
global curvature_shader
global islandid_shader
global objcol_shader
global thickness_shader
global vertcol_shader
global worldpos_shader
global osl_curvature
global osl_height
global post_proc_mat
global post_proc_col
global billboard_mat
global mask_pass_shader
bevmask_shader = file_to.materials[0]
bevnormemit_shader = file_to.materials[1]
bevnormnorm_shader = file_to.materials[2]
cavity_shader = file_to.materials[3]
curvature_shader = file_to.materials[4]
islandid_shader = file_to.materials[5]
objcol_shader = file_to.materials[6]
thickness_shader = file_to.materials[7]
vertcol_shader = file_to.materials[8]
worldpos_shader = file_to.materials[9]
osl_curvature = file_to.materials[10]
osl_height = file_to.materials[11]
post_proc_mat = file_to.materials[12]
post_proc_col = file_to.materials[13]
billboard_mat = file_to.materials[14]
mask_pass_shader = file_to.materials[15]
global materialid_group
global normals_group
global post_masked_bake
global post_chan_map
global internal_add_mask
global billboard_norm
global bent_norm_group
materialid_group = file_to.node_groups[0]
normals_group = file_to.node_groups[1]
post_masked_bake = file_to.node_groups[2]
post_chan_map = file_to.node_groups[3]
internal_add_mask = file_to.node_groups[4]
billboard_norm = file_to.node_groups[5]
bent_norm_group = file_to.node_groups[6]
global post_scene
global output_scene
post_scene = file_to.scenes[0]
output_scene = file_to.scenes[1]
global material_plane
material_plane = file_to.objects[0]
# Start processing bakery node tree
err = process_tree(args.tree, args.node, args.sock)
# Send comma separated list of files written
file_list_str = "<PFILE>"
for file in images_saved:
file_list_str += file
if file != images_saved[-1]:
file_list_str += ","
file_list_str += "</PFILE>"
_print(file_list_str)
# Send comma separated list of pickled vertex colors
pickle_list_str = "<PVERT>"
for file in pickled_verts:
pickle_list_str += file
if file != pickled_verts[-1]:
pickle_list_str += ","
pickle_list_str += "</PVERT>"
_print(pickle_list_str)
# Send end tag
if err:
_print("<ERRORS>", tag=True)
else:
_print("<FINISH>", tag=True)
# Save changes to the file for debugging and exit
if debug:
bpy.ops.wm.save_mainfile(filepath=bpy.data.filepath, exit=True)
return 0
if __name__ == "__main__":
import os.path
import math
import random
import bpy
from datetime import datetime
'''try:
from BakeWrangler.nodes import node_tree
from BakeWrangler.nodes.node_tree import _print
from BakeWrangler.nodes.node_tree import material_recursor
from BakeWrangler.nodes.node_tree import get_input
from BakeWrangler.nodes.node_tree import follow_input_link
from BakeWrangler.nodes.node_tree import gather_output_links
import BakeWrangler.marginer as marginer
except:'''
# Need to import this stuff without referencing the BW module so marginer doesn't try to import the module
import sys
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from nodes import node_tree
from nodes.node_tree import _print
from nodes.node_tree import material_recursor
from nodes.node_tree import get_input
from nodes.node_tree import follow_input_link
from nodes.node_tree import gather_output_links
from vert import ipc
import marginer
main()