5218 lines
250 KiB
Python
5218 lines
250 KiB
Python
import os
|
|
import sys
|
|
import threading, queue
|
|
import subprocess
|
|
from datetime import datetime, timedelta
|
|
import bpy
|
|
from bpy.types import NodeTree, Node, NodeSocket, NodeSocketColor, NodeSocketFloat, NodeFrame, NodeReroute
|
|
try:
|
|
from BakeWrangler.status_bar.status_bar_icon import ensure_bw_icon as update_status_bar
|
|
except:
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
from status_bar.status_bar_icon import ensure_bw_icon as update_status_bar
|
|
|
|
BW_TREE_VERSION = 9
|
|
BW_VERSION_STRING = '1.5.b11'
|
|
|
|
|
|
# Message formatter
|
|
def _print(str, node=None, ret=False, tag=False, wrap=True, enque=None, textdata="BakeWrangler"):
|
|
output = "%s" % (str)
|
|
endl = ''
|
|
flsh = False
|
|
|
|
if node:
|
|
output = "[%s]: %s" % (node.get_name(), output)
|
|
|
|
if tag:
|
|
output.replace("<", "<_")
|
|
output = "<%s>%s" % ("PBAKE", output)
|
|
flsh = True
|
|
if wrap:
|
|
output = "%s</%s>" % (output, "PWRAP")
|
|
else:
|
|
output = "%s</%s>" % (output, "PBAKE")
|
|
|
|
if wrap:
|
|
endl = '\n'
|
|
|
|
if enque != None:
|
|
eout = "%s%s" % (output, endl)
|
|
enque.put(eout)
|
|
return None
|
|
|
|
if ret:
|
|
return output
|
|
|
|
if textdata != None and _prefs('text_msgs'):
|
|
if not textdata in bpy.data.texts.keys():
|
|
bpy.data.texts.new(textdata)
|
|
text = bpy.data.texts[textdata]
|
|
end = len(text.lines[len(text.lines) - 1].body) - 1
|
|
text.cursor_set(len(text.lines) - 1, character=end)
|
|
tout = "%s%s" % (output, endl)
|
|
text.write(tout)
|
|
|
|
print(output, end=endl, flush=flsh)
|
|
|
|
|
|
|
|
# Preference reader
|
|
default_true = ["text_msgs", "clear_msgs", "def_filter_mesh", "def_filter_curve", "def_filter_surface",
|
|
"def_filter_meta", "def_filter_font", "def_filter_light", "auto_open", "fact_start", "show_icon"]
|
|
default_false = ["def_filter_collection", "def_show_adv", "ignore_vis", "make_dirs", "wind_close",
|
|
"invert_bakemod", "wind_msgs", "save_packed", "save_images",]
|
|
default_res = ["def_xres", "def_yres", "def_xout", "def_yout",]
|
|
default_zero = ["def_margin", "def_mask_margin", "def_max_ray_dist", "retrys",]
|
|
def _prefs(key):
|
|
try:
|
|
name = __package__.split('.')
|
|
prefs = bpy.context.preferences.addons[name[0]].preferences
|
|
except:
|
|
pref = False
|
|
else:
|
|
pref = True
|
|
|
|
if pref and key in prefs:
|
|
return prefs[key]
|
|
else:
|
|
# Default values to fall back on
|
|
if key == 'debug':
|
|
if pref:
|
|
return False
|
|
else:
|
|
#return False
|
|
return True
|
|
elif key in default_true:
|
|
return True
|
|
elif key in default_false:
|
|
return False
|
|
elif key in default_res:
|
|
return 1024
|
|
elif key in default_zero:
|
|
return 0
|
|
elif key == 'def_device':
|
|
return 0 # CPU
|
|
elif key == 'def_samples':
|
|
return 1
|
|
elif key == 'def_format':
|
|
return 2 # PNG
|
|
elif key == 'def_raydist':
|
|
return 0.01
|
|
elif key == 'def_outpath':
|
|
return ""
|
|
elif key == 'def_outname':
|
|
return "Image"
|
|
elif key == 'img_non_color':
|
|
return None
|
|
else:
|
|
return None
|
|
|
|
|
|
|
|
# Material validation recursor (takes a shader node and descends the tree via recursion)
|
|
def material_recursor(node, link=None, parent=None):
|
|
# Accepted node types are OUTPUT_MATERIAL, BSDF_PRINCIPLED, MIX/ADD_SHADER and GROUP (plus REROUTE)
|
|
if node.type == 'BSDF_PRINCIPLED':
|
|
return True
|
|
if node.type == 'OUTPUT_MATERIAL' and node.inputs['Surface'].is_linked:
|
|
return material_recursor(node.inputs['Surface'].links[0].from_node, node.inputs['Surface'].links[0], parent)
|
|
if node.type in ['MIX_SHADER', 'ADD_SHADER']:
|
|
if node.type == 'MIX_SHADER':
|
|
input1 = 1
|
|
input2 = 2
|
|
else:
|
|
input1 = 0
|
|
input2 = 1
|
|
inputA = False
|
|
if node.inputs[input1].is_linked:
|
|
inputA = material_recursor(node.inputs[input1].links[0].from_node, node.inputs[input1].links[0], parent)
|
|
inputB = False
|
|
if node.inputs[input2].is_linked:
|
|
inputB = material_recursor(node.inputs[input2].links[0].from_node, node.inputs[input2].links[0], parent)
|
|
return inputA and inputB
|
|
if node.type == 'REROUTE' and node.inputs[0].is_linked:
|
|
return material_recursor(node.inputs[0].links[0].from_node, node.inputs[0].links[0], parent)
|
|
if node.type == 'GROUP' and link:
|
|
# Entering a group, requires similar steps to exiting, but will duplicate code for now
|
|
if parent:
|
|
# Parent modification is always performed on a copy due to branching of recursion
|
|
gparent = parent.copy()
|
|
gparent.append(node)
|
|
else:
|
|
gparent = [node]
|
|
gout = None
|
|
gsoc = 0
|
|
# Find active group socket to begin from. Names may not be unique, so get index
|
|
for soc in node.outputs:
|
|
if link.from_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
|
|
if gout and gout.inputs[gsoc].is_linked:
|
|
return material_recursor(gout.inputs[gsoc].links[0].from_node, gout.inputs[gsoc].links[0], gparent)
|
|
if node.type == 'GROUP_INPUT' and link and parent:
|
|
# Exiting a group, requires similar steps to entering, but will duplicate code for now
|
|
# Parent modification is always performed on a copy due to branching of recursion
|
|
gparent = parent.copy()
|
|
gout = gparent.pop()
|
|
gsoc = 0
|
|
for soc in node.outputs:
|
|
if link.from_socket == soc: break
|
|
else: gsoc += 1
|
|
if gout and gout.inputs[gsoc].is_linked:
|
|
return material_recursor(gout.inputs[gsoc].links[0].from_node, gout.inputs[gsoc].links[0], gparent)
|
|
return False
|
|
|
|
|
|
|
|
# Return the node connected to an input, dealing with re-routes
|
|
def get_input(input):
|
|
if not input.is_output and input.islinked() and input.valid:
|
|
link = follow_input_link(input.links[0])
|
|
return link.from_node
|
|
return None
|
|
|
|
|
|
|
|
# Prune error messages to remove duplicates
|
|
def prune_messages(messages):
|
|
unique = []
|
|
for msg in messages:
|
|
if not unique.count(msg):
|
|
unique.append(msg)
|
|
return unique
|
|
|
|
|
|
|
|
# Prune a list of objects to remove duplicate references
|
|
def prune_objects(objs, allow_dups=False):
|
|
count = []
|
|
dups = []
|
|
# First remove duplicates
|
|
for obj in objs:
|
|
if objs.count(obj) > 1:
|
|
objs.remove(obj)
|
|
# Then remove non duplicate entries that reference the same object where appropriate
|
|
for obj in objs:
|
|
# Get a list of just the referenced objects to count them
|
|
count.append(obj[0])
|
|
for obj in count:
|
|
# Create a list of objects with multiple refs and count how many
|
|
if count.count(obj) > 1:
|
|
found = False
|
|
for dup in dups:
|
|
if dup[0] == obj:
|
|
found = True
|
|
dup[1] += 1
|
|
break
|
|
if not found:
|
|
dups.append([obj, 1])
|
|
for obj in dups:
|
|
# Go over all the duplicate entries and prune appropriately
|
|
num = obj[1]
|
|
for dup in objs:
|
|
if dup[0] == obj[0]:
|
|
# For target set, remove only dups that came from a group (the user may
|
|
# want the same object with different settings)
|
|
if allow_dups:
|
|
if len(dup) == 1:
|
|
objs.remove(dup)
|
|
num -= 1
|
|
# For other sets just reduce to one reference
|
|
else:
|
|
objs.remove(dup)
|
|
num -= 1
|
|
# Break out when/if one dup remains
|
|
if num == 1:
|
|
break
|
|
|
|
# Return pruned object list
|
|
return objs
|
|
|
|
|
|
|
|
# Follow an input link through any reroutes
|
|
def follow_input_link(link):
|
|
if link.from_node.type == 'REROUTE':
|
|
if link.from_node.inputs[0].is_linked:
|
|
try: # During link insertion this can have weird states
|
|
return follow_input_link(link.from_node.inputs[0].links[0])
|
|
except:
|
|
pass
|
|
return link
|
|
|
|
|
|
|
|
# Gather all links from an output, going past any reroutes
|
|
def gather_output_links(output):
|
|
links = []
|
|
for link in output.links:
|
|
if link.is_valid:
|
|
if link.to_node.type == 'REROUTE':
|
|
if link.to_node.outputs[0].is_linked:
|
|
links += gather_output_links(link.to_node.outputs[0])
|
|
else:
|
|
links.append(link)
|
|
return links
|
|
|
|
|
|
# Switch mode to object/back again - Returns the mode being switched from unless no swich is needed
|
|
def switch_mode(mode='OBJECT'):
|
|
curr_mode = bpy.context.mode
|
|
# Convert mode to mode set enum (why are these different?!)
|
|
if curr_mode in ['OBJECT', 'SCULPT']:
|
|
enum_mode = curr_mode
|
|
elif curr_mode.startswith('PAINT_'):
|
|
enum_mode = curr_mode[6:] + "_PAINT"
|
|
else:
|
|
enum_mode = 'EDIT'
|
|
|
|
if enum_mode != mode:
|
|
bpy.ops.object.mode_set(mode=mode)
|
|
return enum_mode
|
|
else:
|
|
return None
|
|
|
|
|
|
#
|
|
# Bake Wrangler Operators
|
|
#
|
|
|
|
# Base class for all bakery operators, provides data to find owning node, etc.
|
|
class BakeWrangler_Operator:
|
|
# Use strings to store their names, since Node isn't a subclass of ID it can't be stored as a pointer
|
|
tree: bpy.props.StringProperty()
|
|
node: bpy.props.StringProperty()
|
|
sock: bpy.props.IntProperty(default=-1)
|
|
|
|
@classmethod
|
|
def poll(type, context):
|
|
return True
|
|
if context.area is not None:
|
|
return True
|
|
#return context.area.type == "NODE_EDITOR" and context.space_data.tree_type == "BakeWrangler_Tree"
|
|
else:
|
|
return False
|
|
|
|
|
|
# Dummy operator to draw when no vertex colors are in list
|
|
class BakeWrangler_Operator_Dummy_VCol(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''No vertex data currently in cache'''
|
|
bl_idname = "bake_wrangler.dummy_vcol"
|
|
bl_label = ""
|
|
|
|
@classmethod
|
|
def poll(type, context):
|
|
# This operator is always supposed to be disabled
|
|
return False
|
|
|
|
|
|
|
|
# Dummy operator to draw when a bake is in progress
|
|
class BakeWrangler_Operator_Dummy(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Bake currently in progress, either cancel the current bake or wait for it to finish'''
|
|
bl_idname = "bake_wrangler.dummy"
|
|
bl_label = ""
|
|
|
|
@classmethod
|
|
def poll(type, context):
|
|
# This operator is always supposed to be disabled
|
|
return False
|
|
|
|
|
|
|
|
# Contol filter selection by allowing modifier keys
|
|
class BakeWrangler_Operator_FilterToggle(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Ctrl-Click to deselect others, Shift-Click to select all others'''
|
|
bl_idname = "bake_wrangler.filter_toggle"
|
|
bl_label = ""
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
filters = ('filter_mesh',
|
|
'filter_curve',
|
|
'filter_surface',
|
|
'filter_meta',
|
|
'filter_font',
|
|
'filter_light')
|
|
|
|
filter: bpy.props.StringProperty()
|
|
|
|
@classmethod
|
|
def description(self, context, properties):
|
|
return properties.filter.split("_")[1].title() + " filter toggle. Ctrl-Click to deselect others, Shift-Click to select all others"
|
|
|
|
def execute(self, context):
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
mod_shift = event.shift
|
|
mod_ctrl = event.ctrl
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
if node and node.bl_idname == 'BakeWrangler_Input_ObjectList':
|
|
# Toggle
|
|
if not mod_ctrl and not mod_shift:
|
|
setattr(node, self.filter, not getattr(node, self.filter))
|
|
# Enable self and disable others
|
|
elif mod_ctrl and not mod_shift:
|
|
for fltr in self.filters:
|
|
setattr(node, fltr, False)
|
|
setattr(node, self.filter, True)
|
|
# Disable self and enable others
|
|
elif not mod_ctrl and mod_shift:
|
|
for fltr in self.filters:
|
|
setattr(node, fltr, True)
|
|
setattr(node, self.filter, False)
|
|
# Invert current states
|
|
elif mod_ctrl and mod_shift:
|
|
for fltr in self.filters:
|
|
setattr(node, fltr, not getattr(node, fltr))
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
# Double/Halve value
|
|
class BakeWrangler_Operator_DoubleVal(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Description set by function'''
|
|
bl_idname = "bake_wrangler.double_val"
|
|
bl_label = ""
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
val: bpy.props.StringProperty()
|
|
half: bpy.props.BoolProperty()
|
|
|
|
@classmethod
|
|
def set_props(self, inst, node, tree, value, half=False):
|
|
inst.tree = tree
|
|
inst.node = node
|
|
inst.val = value
|
|
inst.half = half
|
|
|
|
@classmethod
|
|
def description(self, context, properties):
|
|
if properties.half: return "Halve value"
|
|
else: return "Double value"
|
|
|
|
def execute(self, context):
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
value = getattr(node, self.val)
|
|
if self.half: value /= 2
|
|
else: value *= 2
|
|
setattr(node, self.val, int(value))
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
# Pick an enum from a menu
|
|
class BakeWrangler_Operator_PickMenuEnum(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Description set by function'''
|
|
bl_idname = "bake_wrangler.pick_menu_enum"
|
|
bl_label = ""
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
enum_id: bpy.props.StringProperty()
|
|
enum_desc: bpy.props.StringProperty()
|
|
enum_prop: bpy.props.StringProperty()
|
|
|
|
@classmethod
|
|
def set_props(self, inst, e_id, e_desc, e_prop, node, tree):
|
|
inst.tree = tree
|
|
inst.node = node
|
|
inst.enum_prop = e_prop
|
|
inst.enum_desc = e_desc
|
|
inst.enum_id = e_id
|
|
|
|
@classmethod
|
|
def description(self, context, properties):
|
|
return properties.enum_desc
|
|
|
|
def execute(self, context):
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
setattr(node, self.enum_prop, self.enum_id)
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
# Add selected objects to an ObjectList node (ignoring duplicates unless Shift held)
|
|
class BakeWrangler_Operator_AddSelected(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Adds selected objects to the node, respecting filter and ignoring duplicates\nShift-Click: Adds items even if they are duplicates'''
|
|
bl_idname = "bake_wrangler.add_selected"
|
|
bl_label = "Add Selected"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
mod_shift = False
|
|
|
|
# Called either after invoke from UI or directly from script
|
|
def execute(self, context):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
|
|
existing_objs = []
|
|
# Get all the objects in the current node (ignoring connected nodes and groups)
|
|
for input in node.inputs:
|
|
if not input.is_linked and input.value:
|
|
existing_objs.append(input.value)
|
|
|
|
selected_objs = []
|
|
# Get a list of all selected objects that also match current filter
|
|
for obj in context.selected_objects:
|
|
if node.input_filter(obj.name, obj):
|
|
selected_objs.append(obj)
|
|
|
|
# Add non duplicate objects to the end of the node (includes duplicates if Shift)
|
|
for obj in selected_objs:
|
|
if self.mod_shift or obj not in existing_objs:
|
|
node.inputs[-1].value = obj
|
|
|
|
return {'FINISHED'}
|
|
|
|
# Called from button press, set modifier key states
|
|
def invoke(self, context, event):
|
|
self.mod_shift = event.shift
|
|
return self.execute(context)
|
|
|
|
|
|
# Read vertex color data from temp files and apply to current file
|
|
class BakeWrangler_Operator_DiscardBakedVertCols(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Discard baked vertex color data'''
|
|
bl_idname = "bake_wrangler.discard_vertcols"
|
|
bl_label = "Discard Data"
|
|
|
|
# Read and apply vertex colors
|
|
def execute(self, context):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
if node.bl_idname not in ['BakeWrangler_Output_Vertex_Cols', 'BakeWrangler_Output_Batch_Bake']:
|
|
return {'CANCELLED'}
|
|
files = node.vert_files
|
|
while len(node.vert_files):
|
|
jar = node.vert_files.pop()
|
|
try:
|
|
os.remove(jar)
|
|
except:
|
|
pass
|
|
|
|
self.report({'INFO'}, "Data Removed")
|
|
return {'FINISHED'}
|
|
|
|
# Confirm the action
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
|
|
# Read vertex color data from temp files and apply to current file
|
|
class BakeWrangler_Operator_ApplyBakedVertCols(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Apply baked vertex color data to current blend file objects'''
|
|
bl_idname = "bake_wrangler.apply_vertcols"
|
|
bl_label = "Apply Data"
|
|
|
|
# Read and apply vertex colors
|
|
def execute(self, context):
|
|
try:
|
|
from BakeWrangler.vert import ipc
|
|
except:
|
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
|
from vert import ipc
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
if node.bl_idname not in ['BakeWrangler_Output_Vertex_Cols', 'BakeWrangler_Output_Batch_Bake']:
|
|
return {'CANCELLED'}
|
|
files = node.vert_files
|
|
if not len(files):
|
|
return {'CANCELLED'}
|
|
|
|
# Open each file and apply the data one at a time
|
|
oerr = 0
|
|
for jar in files:
|
|
fd = ipc.open_pickle_jar(file=jar)
|
|
data = ipc.depickle_verts(file=fd)
|
|
err, str = ipc.import_verts(cols=data)
|
|
if err:
|
|
_print("Error applying vertex data: %s" % (str), node=node)
|
|
oerr += 1
|
|
else:
|
|
_print("Applied %s" % (str), node=node)
|
|
if fd: fd.close()
|
|
|
|
if oerr:
|
|
self.report({'ERROR'}, "Apply Failed")
|
|
return {'CANCELLED'}
|
|
|
|
# Remove temp files if no errors
|
|
while len(node.vert_files):
|
|
jar = node.vert_files.pop()
|
|
try:
|
|
os.remove(jar)
|
|
except:
|
|
pass
|
|
|
|
self.report({'INFO'}, "Data Applied")
|
|
return {'FINISHED'}
|
|
|
|
# Confirm the action
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
|
|
# Kill switch to stop a bake in progress
|
|
class BakeWrangler_Operator_BakeStop(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Cancel currently running bake'''
|
|
bl_idname = "bake_wrangler.bake_stop"
|
|
bl_label = "Cancel Bake"
|
|
|
|
# Stop the currently running bake
|
|
def execute(self, context):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
if tree.baking != None:
|
|
tree.baking.stop()
|
|
tree.interface_update(context)
|
|
return {'FINISHED'}
|
|
|
|
# Ask the user if they really want to cancel bake
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_confirm(self, event)
|
|
|
|
|
|
# Operator for bake pass node
|
|
class BakeWrangler_Operator_BakePass(BakeWrangler_Operator, bpy.types.Operator):
|
|
'''Perform requested bake action(s)'''
|
|
bl_idname = "bake_wrangler.bake_pass"
|
|
bl_label = "Bake Pass"
|
|
|
|
_timer = None
|
|
|
|
_thread = None
|
|
_kill = False
|
|
_success = False
|
|
_finish = False
|
|
_lock = threading.Lock()
|
|
_queue = queue.SimpleQueue()
|
|
_ifileq = queue.SimpleQueue()
|
|
_vfileq = queue.SimpleQueue()
|
|
stopping = False
|
|
|
|
open_win = None
|
|
open_ed = None
|
|
node_ed = None
|
|
|
|
start = None
|
|
valid = None
|
|
blend_copy = None
|
|
blend_log = None
|
|
bake_proc = None
|
|
was_dirty = False
|
|
img_list = []
|
|
vert_list = []
|
|
shutdown = False
|
|
|
|
# Stop this bake if it's currently running
|
|
def stop(self, kill=True):
|
|
if self._thread and self._thread.is_alive() and kill:
|
|
with self._lock:
|
|
self.stopping = self._kill = True
|
|
return self.stopping
|
|
|
|
# Runs a blender subprocess
|
|
def thread(self, node_name, tree_name, file_name, exec_name, script_name):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
sock = self.sock
|
|
debug = _prefs('debug')
|
|
ignorevis = _prefs('ignore_vis')
|
|
factory = ""
|
|
rend_dev_str = ""
|
|
rend_dev_val = ""
|
|
rend_use_str = ""
|
|
rend_use_val = ""
|
|
solution_itr = 0
|
|
frames_itr = 0
|
|
batch_itr = 0
|
|
retry = _prefs('retrys') + 2
|
|
if _prefs('fact_start'):
|
|
factory = "--factory-startup"
|
|
# Need to reselect gpu render some how when fact starting
|
|
rend_type = bpy.context.preferences.addons['cycles'].preferences.compute_device_type
|
|
rend_use = ""
|
|
for dev in bpy.context.preferences.addons['cycles'].preferences.devices:
|
|
if dev.use: rend_use = "%s1" % rend_use
|
|
else: rend_use = "%s0" % rend_use
|
|
rend_dev_str = "--rend_dev"
|
|
rend_dev_val = str(rend_type)
|
|
rend_use_str = "--rend_use"
|
|
rend_use_val = str(rend_use)
|
|
|
|
_print("Launching background process:", node=node, enque=self._queue)
|
|
_print("================================================================================", enque=self._queue)
|
|
while not self._finish and retry > 0:
|
|
sub = subprocess.Popen([
|
|
'blender',
|
|
file_name,
|
|
"--background",
|
|
"--python", script_name,
|
|
factory,
|
|
"--",
|
|
"--tree", tree_name,
|
|
"--node", node_name,
|
|
"--sock", str(int(sock)),
|
|
"--debug", str(int(debug)),
|
|
"--ignorevis", str(int(ignorevis)),
|
|
"--solitr", str(solution_itr),
|
|
"--frameitr", str(frames_itr),
|
|
"--batchitr", str(batch_itr),
|
|
rend_dev_str, rend_dev_val,
|
|
rend_use_str, rend_use_val,
|
|
], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8", errors="replace")
|
|
|
|
# Read output from subprocess and print tagged lines
|
|
out = ""
|
|
kill = False
|
|
while sub.poll() == None:
|
|
# Check for kill flag
|
|
if self._lock.acquire(blocking=False):
|
|
if self._kill:
|
|
_print("Bake canceled, terminating process...", enque=self._queue)
|
|
sub.kill()
|
|
out, err = sub.communicate()
|
|
kill = True
|
|
self._lock.release()
|
|
|
|
if not kill:
|
|
out = sub.stdout.read(1)
|
|
# Collect tagged lines and display them in console
|
|
if out == '<':
|
|
out += sub.stdout.read(6)
|
|
if out == "<PBAKE>":
|
|
tag_end = False
|
|
tag_line = ""
|
|
out = ""
|
|
tag_out = ""
|
|
# Read until end tag is found
|
|
while not tag_end:
|
|
tag = sub.stdout.read(1)
|
|
|
|
if tag == '<':
|
|
tag_line = sub.stdout.read(1)
|
|
if tag_line != '_':
|
|
tag_line = tag + tag_line + sub.stdout.read(6)
|
|
if tag_line == "</PBAKE>":
|
|
tag_end = True
|
|
out += '\n'
|
|
elif tag_line == "</PWRAP>":
|
|
tag_end = True
|
|
tag_out += '\n'
|
|
#sys.stdout.write('\n')
|
|
#sys.stdout.flush()
|
|
elif tag_line == "<FINISH>":
|
|
tag_line += sub.stdout.read(8)
|
|
tag_end = True
|
|
self._success = True
|
|
self._finish = True
|
|
elif tag_line == "<ERRORS>":
|
|
tag_line += sub.stdout.read(8)
|
|
tag_end = True
|
|
self._success = False
|
|
self._finish = True
|
|
if tag != '' and not tag_end:
|
|
tag_out += tag
|
|
#sys.stdout.write(tag_line)
|
|
#sys.stdout.flush()
|
|
out += tag
|
|
_print(tag_out, enque=self._queue, wrap=False)
|
|
if out == "<PFILE>" or out == "<PVERT>":
|
|
tag_end = False
|
|
tag_line = ""
|
|
files = ""
|
|
# Set output queue
|
|
if out == "<PFILE>":
|
|
que = self._ifileq
|
|
else:
|
|
que = self._vfileq
|
|
while not tag_end:
|
|
tag = sub.stdout.read(1)
|
|
if tag == '<':
|
|
tag_line = sub.stdout.read(1)
|
|
if tag_line != '_':
|
|
tag_line = tag + tag_line + sub.stdout.read(6)
|
|
if tag_line == "</PFILE>" or tag_line == "</PVERT>":
|
|
tag_end = True
|
|
_print(files, enque=que, wrap=False)
|
|
if tag != '' and not tag_end:
|
|
files += tag
|
|
out = ''
|
|
if out == "<PFRAM>" or out == "<PSOLU>" or out == "<PBATC>":
|
|
tag_end = False
|
|
tag_line = ""
|
|
num = ""
|
|
while not tag_end:
|
|
tag = sub.stdout.read(1)
|
|
if tag == '<':
|
|
tag_line = sub.stdout.read(1)
|
|
if tag_line != '_':
|
|
tag_line = tag + tag_line + sub.stdout.read(6)
|
|
if tag_line == "</PFRAM>":
|
|
tag_end = True
|
|
frames_itr = int(num)
|
|
elif tag_line == "</PSOLU>":
|
|
tag_end = True
|
|
solution_itr = int(num)
|
|
elif tag_line == "</PBATC>":
|
|
tag_end = True
|
|
batch_itr = int(num)
|
|
if tag != '' and not tag_end:
|
|
num += tag
|
|
out = ''
|
|
# Write to log
|
|
if out != '' and self.blend_log:
|
|
self.blend_log.write(out)
|
|
self.blend_log.flush()
|
|
_print("================================================================================", enque=self._queue)
|
|
_print("Background process ended", node=node, enque=self._queue)
|
|
retry -= 1
|
|
if not self._finish and retry > 0:
|
|
_print("Process did not complete, retry from last known success (%s tries remain)" % (retry - 1), node=node, enque=self._queue)
|
|
|
|
# Event handler
|
|
def modal(self, context, event):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
|
|
# Check if the bake thread has ended every timer event
|
|
if event.type == 'TIMER':
|
|
self.print_queue(context)
|
|
# Reapply dirt by pushing something to undo stack (not ideal)
|
|
if self.was_dirty and not bpy.data.is_dirty and bpy.ops.node.select_all.poll():
|
|
bpy.ops.node.select_all(action='INVERT')
|
|
bpy.ops.node.select_all(True, action='INVERT')
|
|
self.was_dirty = False
|
|
if not self._thread.is_alive():
|
|
self.cancel(context)
|
|
if self._kill:
|
|
_print("Bake canceled after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
|
|
_print("Canceled\n", node=node, enque=self._queue)
|
|
self.report({'WARNING'}, "Bake Canceled")
|
|
self.update_images()
|
|
self.print_queue(context)
|
|
if self.blend_log:
|
|
context.window_manager.bw_lastlog = self.blend_copy + ".log"
|
|
context.window_manager.bw_lastfile = self.blend_copy
|
|
return {'CANCELLED'}
|
|
else:
|
|
if self._success and self._finish:
|
|
_print("Bake finished in %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
|
|
_print("Success\n", node=node, enque=self._queue)
|
|
self.report({'INFO'}, "Bake Completed")
|
|
context.window_manager.bw_status = 0 # Bake finished / idle status
|
|
elif self._finish:
|
|
_print("Bake finished with errors after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
|
|
_print("Errors\n", node=node, enque=self._queue)
|
|
self.report({'WARNING'}, "Bake Finished with Errors")
|
|
context.window_manager.bw_status = 2 # Bake error status
|
|
else:
|
|
_print("Bake failed after %s" % (str(datetime.now() - self.start)), node=node, enque=self._queue)
|
|
_print("Failed\n", node=node, enque=self._queue)
|
|
self.report({'ERROR'}, "Bake Failed")
|
|
context.window_manager.bw_status = 2 # Bake error status
|
|
self.print_queue(context)
|
|
if self.blend_log:
|
|
context.window_manager.bw_lastlog = self.blend_copy + ".log"
|
|
context.window_manager.bw_lastfile = self.blend_copy
|
|
# Update images
|
|
self.dequeue_files(context, self._ifileq, self.img_list)
|
|
if _prefs('debug'): _print("Img list: %s" % self.img_list)
|
|
self.update_images()
|
|
# Send vertex file names to node
|
|
if hasattr(node, 'vert_files'):
|
|
self.dequeue_files(context, self._vfileq, self.vert_list)
|
|
if _prefs('debug'): _print("Vert list: %s" % self.vert_list)
|
|
for vfile in self.vert_list:
|
|
node.vert_files.append(vfile)
|
|
# Check if a post-bake user script should be run
|
|
if self._finish and node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.loc_post != 'NON':
|
|
_print("Running user created post-bake script: ", node=node, wrap=False)
|
|
if node.loc_post == 'INT':
|
|
post_scr = node.script_post_int.as_string()
|
|
elif node.loc_post == 'EXT':
|
|
with open(node.script_post_can, "r") as scr:
|
|
post_scr = scr.read()
|
|
try:
|
|
exec(post_scr, {'BW_TARGETS': node.get_unique_objects('TARGET'), 'BW_SOURCES': node.get_unique_objects('SOURCE')})
|
|
except Exception as err:
|
|
_print(" Failed - %s" % (str(err)))
|
|
else:
|
|
_print(" Done")
|
|
if _prefs("wind_msgs") and self.open_win:
|
|
if _prefs("wind_close"):
|
|
bpy.ops.wm.window_close({"window": self.open_win})
|
|
# Do batch shutdown
|
|
if self.shutdown:
|
|
if sys.platform == 'win32':
|
|
os.system('shutdown /s /t 60')
|
|
else:
|
|
os.system('sudo shutdown +1')
|
|
if _prefs("show_icon"): update_status_bar()
|
|
return {'FINISHED'}
|
|
return {'PASS_THROUGH'}
|
|
|
|
# Get queued file list
|
|
def dequeue_files(self, context, queue, list):
|
|
fstr = ""
|
|
try:
|
|
# An Empty exception will be raised when nothing is in the queue
|
|
while True:
|
|
fstr += queue.get_nowait()
|
|
except:
|
|
list += fstr.split(",")
|
|
return
|
|
|
|
# Print queued messages
|
|
def print_queue(self, context):
|
|
try:
|
|
# An Empty exception will be raised when nothing is in the queue
|
|
while True:
|
|
msg = self._queue.get_nowait()
|
|
_print(msg, wrap=False)
|
|
except:
|
|
return
|
|
|
|
# Display log file if debugging is enabled and the bake failed or had errors
|
|
def show_log(self):
|
|
if _prefs('debug') and self.blend_log and self.node_ed:
|
|
bpy.ops.screen.area_dupli({'area': self.node_ed}, 'INVOKE_DEFAULT')
|
|
open_ed = bpy.context.window_manager.windows[len(bpy.context.window_manager.windows) - 1].screen.areas[0]
|
|
open_ed.type = 'TEXT_EDITOR'
|
|
log = bpy.data.texts.load(self.blend_copy + ".log")
|
|
open_ed.spaces[0].text = log
|
|
open_ed.spaces[0].show_line_numbers = False
|
|
open_ed.spaces[0].show_syntax_highlight = False
|
|
|
|
# Update any loaded images that might be changed by the bake
|
|
def update_images(self):
|
|
if len(self.img_list):
|
|
cwd = os.path.dirname(bpy.data.filepath)
|
|
open_imgs = {}
|
|
for img in bpy.data.images:
|
|
open_imgs[os.path.normpath(os.path.join(cwd, bpy.path.abspath(img.filepath_raw)))] = img
|
|
for img in self.img_list:
|
|
if img in open_imgs.keys():
|
|
open_imgs[img].reload()
|
|
elif _prefs("auto_open"):
|
|
try:
|
|
bpy.data.images.load(img)
|
|
except:
|
|
pass
|
|
|
|
# Called after invoke to perform the bake if everything passed validation
|
|
def execute(self, context):
|
|
# If called from script, do prepare now
|
|
if self.valid == None:
|
|
if self.tree and self.node:
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
self.prepare(context, tree, node)
|
|
else:
|
|
self.valid = [False]
|
|
self.valid.append([_print("Bake Tree/Node missing", ret=True), ": Bake Tree or Node was not set for operator"])
|
|
self.report({'ERROR'}, "Operator required arguments missing")
|
|
return {'CANCELLED'}
|
|
# Do any interactive actions if called from invoke
|
|
else:
|
|
# If message log in new window is enabled
|
|
if _prefs("text_msgs") and _prefs("wind_msgs"):
|
|
bpy.ops.screen.area_dupli('INVOKE_DEFAULT')
|
|
self.open_win = context.window_manager.windows[len(context.window_manager.windows) - 1]
|
|
self.open_ed = self.open_win.screen.areas[0]
|
|
self.open_ed.type = 'TEXT_EDITOR'
|
|
self.open_ed.spaces[0].text = bpy.data.texts["BakeWrangler"]
|
|
self.open_ed.spaces[0].show_line_numbers = False
|
|
self.open_ed.spaces[0].show_syntax_highlight = False
|
|
|
|
if not self.valid[0]:
|
|
self.cancel(context)
|
|
self.report({'ERROR'}, "Validation failed")
|
|
return {'CANCELLED'}
|
|
|
|
self.start = datetime.now()
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
|
|
# Check for batch shutdown
|
|
if node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.shutdown_after:
|
|
self.shutdown = True
|
|
|
|
# Save a temporary copy of the blend file and store the path. Make sure the path doesn't exist first.
|
|
# All baking will be done using this copy so the user can continue working in this session.
|
|
blend_name = bpy.path.clean_name(bpy.path.display_name_from_filepath(bpy.data.filepath))
|
|
blend_temp = bpy.path.abspath(bpy.app.tempdir)
|
|
node_cname = bpy.path.clean_name(node.get_name())
|
|
blend_copy = os.path.join(blend_temp, "BW_" + blend_name)
|
|
|
|
# Increment file name until it doesn't exist
|
|
if os.path.exists(blend_copy + ".blend"):
|
|
fno = 1
|
|
while os.path.exists(blend_copy + "_%03i.blend" % (fno)):
|
|
fno = fno + 1
|
|
blend_copy = blend_copy + "_%03i.blend" % (fno)
|
|
else:
|
|
blend_copy = blend_copy + ".blend"
|
|
|
|
# Check if a pre-bake user script should be run
|
|
if node.bl_idname == 'BakeWrangler_Output_Batch_Bake' and node.loc_pre != 'NON':
|
|
_print("Running user created pre-bake script: ", node=node, wrap=False)
|
|
if node.loc_pre == 'INT':
|
|
pre_scr = node.script_pre_int.as_string()
|
|
elif node.loc_pre == 'EXT':
|
|
with open(node.script_pre_can, "r") as scr:
|
|
pre_scr = scr.read()
|
|
try:
|
|
exec(pre_scr, {'BW_TARGETS': node.get_unique_objects('TARGET'), 'BW_SOURCES': node.get_unique_objects('SOURCE')})
|
|
except Exception as err:
|
|
_print(" Failed - %s" % (str(err)))
|
|
return {'CANCELLED'}
|
|
else:
|
|
_print(" Done")
|
|
|
|
# Print out start message and temp path
|
|
_print("")
|
|
_print("=== Bake starts ===", node=node)
|
|
_print("Creating temporary files in %s" % (blend_temp), node=node)
|
|
|
|
# Maintain dirt
|
|
if bpy.data.is_dirty:
|
|
self.was_dirty = True
|
|
|
|
# Save dirty images based on preferences as unsaved changes will not effect bake
|
|
if _prefs("save_packed") or _prefs("save_images"):
|
|
for img in bpy.data.images:
|
|
if img.is_dirty:
|
|
if (img.packed_file is not None and _prefs("save_packed")) or (img.packed_file is None and _prefs("save_images")):
|
|
bpy.ops.image.save({'edit_image': img})
|
|
|
|
try:
|
|
bpy.ops.wm.save_as_mainfile(filepath=blend_copy, copy=True)
|
|
except RuntimeError as err:
|
|
_print("Temporary file creation failed: %s" % (str(err)), node=node)
|
|
self.report({'ERROR'}, "Blend file copy failed")
|
|
return {'CANCELLED'}
|
|
else:
|
|
# Check copy exists
|
|
if not os.path.exists(blend_copy):
|
|
_print("Temporary file creation failed", node=node)
|
|
self.report({'ERROR'}, "Blend file copy failed")
|
|
return {'CANCELLED'}
|
|
else:
|
|
self.blend_copy = blend_copy
|
|
|
|
# Open a log file at the same location with a .log appended to the name
|
|
log_err = None
|
|
blend_log = None
|
|
try:
|
|
blend_log = open(blend_copy + ".log", "w", encoding="utf-8", errors="replace")
|
|
except OSError as err:
|
|
self.report({'WARNING'}, "Couldn't create log file")
|
|
log_err = err.strerror
|
|
else:
|
|
self.blend_log = blend_log
|
|
tree.last_log = blend_copy + ".log"
|
|
|
|
# Print out blend copy and log names
|
|
_print(" - %s" % (os.path.basename(self.blend_copy)), node=node)
|
|
if self.blend_log and not log_err:
|
|
_print(" - %s" % (os.path.basename(self.blend_copy + ".log")), node=node)
|
|
else:
|
|
_print(" - Log file creation failed: %s" % (log_err), node=node)
|
|
_print("Blender: %s Addon: %s" % (bpy.app.version_string, BW_VERSION_STRING), node=node)
|
|
|
|
# Create a thread which will launch a background instance of blender running a script that does all the work.
|
|
# Process is complete when thread exits. Will need full path to blender, node, temp file and proc script.
|
|
blend_exec = bpy.path.abspath(bpy.app.binary_path)
|
|
self._thread = threading.Thread(target=self.thread, args=(self.node, self.tree, self.blend_copy, blend_exec, self.bake_proc,))
|
|
|
|
# Get a list of image file names that will be updated by the bake so they can be reloaded on success
|
|
self.img_list = []
|
|
'''if node.bl_idname == 'BakeWrangler_Output_Batch_Bake':
|
|
for input in node.inputs:
|
|
outnode = get_input(input)
|
|
if outnode and outnode.bl_idname == 'BakeWrangler_Output_Image_Path':
|
|
files = outnode.get_output_files()
|
|
for name in files.keys():
|
|
img_name = os.path.join(outnode.img_path, name)
|
|
if not self.img_list.count(img_name):
|
|
self.img_list.append(img_name)
|
|
elif node.bl_idname == 'BakeWrangler_Output_Image_Path':
|
|
files = node.get_output_files()
|
|
for name in files.keys():
|
|
img_name = os.path.join(node.img_path, name)
|
|
if not self.img_list.count(img_name):
|
|
self.img_list.append(img_name)'''
|
|
|
|
# Init vert file list
|
|
self.vert_list = []
|
|
bpy.ops.bake_wrangler.discard_vertcols(node=self.node, tree=self.tree)
|
|
|
|
# Add a timer to periodically check if the bake has finished
|
|
wm = context.window_manager
|
|
self._timer = wm.event_timer_add(0.5, window=context.window)
|
|
wm.modal_handler_add(self)
|
|
|
|
self._thread.start()
|
|
|
|
# Go modal
|
|
context.window_manager.bw_status = 1 # Baking status
|
|
if _prefs("show_icon"): update_status_bar()
|
|
return {'RUNNING_MODAL'}
|
|
|
|
# Called by UI when the button is clicked. Will validate settings and prepare files for execute
|
|
def invoke(self, context, event):
|
|
# Prep for bake
|
|
self.node_ed = context.area
|
|
if self.tree and self.node:
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
self.prepare(context, tree, node)
|
|
else:
|
|
self.valid = [False]
|
|
self.valid.append([_print("Bake Tree/Node missing", ret=True), ": Bake Tree or Node was not set for operator"])
|
|
self.report({'ERROR'}, "Operator properties not set")
|
|
return {'CANCELLED'}
|
|
|
|
if not self.valid[0] or len(self.valid) > 1:
|
|
# Draw pop-up that will use custom draw function to display any validation errors
|
|
return context.window_manager.invoke_props_dialog(self, width=400)
|
|
else:
|
|
return self.execute(context)
|
|
|
|
# Prepare for bake, do all validation tasks and set properties
|
|
def prepare(self, context, tree, node):
|
|
# Init variables
|
|
self.valid = [False]
|
|
|
|
# Are text editor messages enabled?
|
|
if _prefs("text_msgs"):
|
|
# Make sure the text block exists
|
|
if not "BakeWrangler" in bpy.data.texts.keys():
|
|
bpy.data.texts.new("BakeWrangler")
|
|
# Clear the block if option set
|
|
if _prefs("clear_msgs"):
|
|
bpy.data.texts["BakeWrangler"].clear()
|
|
|
|
# Do full validation of bake so it can be reported
|
|
tree.baking = self
|
|
tree.interface_update(context)
|
|
self.valid = node.validate(is_primary=True)
|
|
|
|
# Remove UV errors if node is a vertex color output, kinda dumb, but... Least changes required this way
|
|
if node.bl_idname == 'BakeWrangler_Output_Vertex_Cols' and not self.valid[0]:
|
|
idx = 0
|
|
for msg in self.valid:
|
|
if idx == 0:
|
|
idx += 1
|
|
continue
|
|
if msg[0].endswith("UV error"):
|
|
self.valid.pop(idx)
|
|
idx += 1
|
|
if len(self.valid) == 1:
|
|
self.valid[0] = True
|
|
|
|
# Check tree is of the current version
|
|
if tree.tree_version != BW_TREE_VERSION:
|
|
self.valid[0] = False
|
|
if tree.tree_version < BW_TREE_VERSION:
|
|
self.valid.append([_print("Bake Recipe for older version of Bake Wrangler", node=node, ret=True), ": Recipe is version %s, but version %s is requied. Please use the auto-update function if available, or create a new recipe" % (tree.tree_version, BW_TREE_VERSION)])
|
|
else:
|
|
self.valid.append([_print("Bake Recipe for newer version of Bake Wrangler", node=node, ret=True), ": Recipe is version %s, but version %s is requied. You need to update Bake Wrangler to a version that supports this recipe, or create a new recipe" % (tree.tree_version, BW_TREE_VERSION)])
|
|
# Check processing script exists
|
|
bake_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
|
bake_proc = bpy.path.abspath(os.path.join(bake_path, "baker.py"))
|
|
if not os.path.exists(bake_proc):
|
|
self.valid[0] = False
|
|
self.valid.append([_print("File missing", node=node, ret=True), ": Bake processing script wasn't found at '%s'" % (bake_proc)])
|
|
else:
|
|
self.bake_proc = bake_proc
|
|
|
|
# Check baking scene file exists
|
|
scene_file = bpy.path.abspath(os.path.join(bake_path, "resources", "BakeWrangler_Scene.blend"))
|
|
if not os.path.exists(scene_file):
|
|
self.valid[0] = False
|
|
self.valid.append([_print("File missing", node=node, ret=True), ": Bake scene library wasn't found at '%s'" % (scene_file)])
|
|
self.valid = prune_messages(self.valid)
|
|
|
|
# Cancel the bake
|
|
def cancel(self, context):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
if self._timer:
|
|
wm = context.window_manager
|
|
wm.event_timer_remove(self._timer)
|
|
if self.blend_log:
|
|
self.blend_log.close()
|
|
if tree.baking != None:
|
|
tree.baking = None
|
|
tree.interface_update(context)
|
|
if self.blend_copy and os.path.exists(self.blend_copy):
|
|
if not _prefs("debug"):
|
|
try:
|
|
os.remove(self.blend_copy)
|
|
except OSError as err:
|
|
_print("Temporary file removal failed: %s\n" % (err.strerror), node=node, enque=self._queue)
|
|
|
|
# Draw custom pop-up
|
|
def draw(self, context):
|
|
tree = bpy.data.node_groups[self.tree]
|
|
node = tree.nodes[self.node]
|
|
layout = self.layout.column(align=True)
|
|
if not self.valid[0]:
|
|
layout.label(text="!!! Validation FAILED:")
|
|
_print("")
|
|
_print("!!! Validation FAILED:", node=node)
|
|
col = layout.column()
|
|
for i in range(len(self.valid) - 1):
|
|
col.label(text=self.valid[i + 1][0])
|
|
_print(self.valid[i + 1][0] + self.valid[i + 1][1])
|
|
layout.label(text="See console for details")
|
|
_print("")
|
|
elif len(self.valid) > 1:
|
|
layout.label(text="")
|
|
layout.label(text="!!! Material Warnings:")
|
|
_print("")
|
|
_print("!!! Material Warnings:")
|
|
col = layout.column()
|
|
for i in range(len(self.valid) - 1):
|
|
col.label(text=self.valid[i + 1][0])
|
|
_print(self.valid[i + 1][0] + self.valid[i + 1][1])
|
|
layout.label(text="See console for details")
|
|
_print("")
|
|
|
|
|
|
|
|
#
|
|
# Bake Wrangler nodes system
|
|
#
|
|
|
|
|
|
# Node tree definition that shows up in the editor type list. Sets the name, icon and description.
|
|
class BakeWrangler_Tree(NodeTree):
|
|
'''Improved baking system to extend and replace existing internal bake system'''
|
|
bl_label = 'Bake Recipe'
|
|
bl_icon = 'NODETREE'
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
# Get pinned mesh settings if exists
|
|
def get_pinned_settings(self, setting):
|
|
for node in self.nodes:
|
|
if node.bl_idname == 'BakeWrangler_' + setting and node.pinned:
|
|
return node
|
|
return None
|
|
|
|
# Get the active global resolution node in a tree
|
|
def get_active_res(self):
|
|
for node in self.nodes:
|
|
if node.bl_idname == 'BakeWrangler_Global_Resolution' and node.is_active:
|
|
return node
|
|
return None
|
|
|
|
# Does this need a lock for modal event access?
|
|
baking = None
|
|
|
|
# Do some initial set up when a new tree is created
|
|
initialised: bpy.props.BoolProperty(name="Initialized", default=False)
|
|
tree_version: bpy.props.IntProperty(name="Tree Version", default=0)
|
|
last_log: bpy.props.StringProperty(name="Last Log", default="")
|
|
|
|
|
|
|
|
# Custom Sockets:
|
|
|
|
# Base class for all bakery sockets
|
|
class BakeWrangler_Tree_Socket:
|
|
# Workaround for link.is_valid being un-usable
|
|
valid: bpy.props.BoolProperty()
|
|
|
|
def socket_label(self, text):
|
|
if self.is_output or (self.is_linked and self.valid) or (not self.is_output and not self.is_linked):
|
|
return text
|
|
else:
|
|
return text + " [invalid]"
|
|
|
|
def socket_color(self, color):
|
|
if not self.is_output and self.is_linked and not self.valid:
|
|
return (1.0, 0.0, 0.0, 1.0)
|
|
else:
|
|
return color
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
return [self.bl_idname]
|
|
|
|
# Follows through reroutes
|
|
def islinked(self):
|
|
if self.is_linked and not self.is_output:
|
|
try: # During link removal this can be in a weird state
|
|
node = self.links[0].from_node
|
|
while node.type == "REROUTE":
|
|
if node.inputs[0].is_linked and node.inputs[0].links[0].is_valid:
|
|
node = node.inputs[0].links[0].from_node
|
|
else:
|
|
return False
|
|
return True
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
|
|
|
|
# Socket for an object or list of objects to be used in a bake pass in some way
|
|
class BakeWrangler_Socket_Object(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for baking relevant objects'''
|
|
bl_label = 'Object'
|
|
|
|
object_types = ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT', 'LIGHT']
|
|
|
|
# Called to filter objects listed in the value search field
|
|
def value_prop_filter(self, object):
|
|
return self.node.input_filter(self.name, object)
|
|
|
|
def cage_prop_filter(self, cage):
|
|
return cage.type == 'MESH'
|
|
|
|
# Try to auto locate the cage by name when enabled
|
|
def use_cage_update(self, context):
|
|
if self.use_cage and not self.cage and self.value:
|
|
for obj in bpy.data.objects:
|
|
if obj.name.startswith(self.value.name) and obj.name.lower().startswith("cage", len(self.value.name) + 1):
|
|
self.cage = obj
|
|
break
|
|
|
|
# Called when the value property changes
|
|
def value_prop_update(self, context):
|
|
self.type = 'NONE'
|
|
if self.value:
|
|
if self.value.rna_type.identifier == 'Collection':
|
|
self.type = 'GROUP'
|
|
elif self.value.rna_type.identifier == 'Object':
|
|
if self.value.type in self.object_types:
|
|
self.type = '%s_DATA' % (self.value.type)
|
|
if self.node:
|
|
self.node.update_inputs()
|
|
|
|
# Get own objects or the full linked tree
|
|
def get_objects(self, only_mesh=False, no_lights=False, only_groups=False):
|
|
objects = []
|
|
# Follow links
|
|
if self.islinked() and self.valid:
|
|
return follow_input_link(self.links[0]).from_node.get_objects(only_mesh, no_lights, only_groups)
|
|
# Otherwise return self values
|
|
if self.value and self.type and self.type != 'NONE' and not self.is_linked:
|
|
# Only interested in mesh types?
|
|
if self.type not in ['MESH_DATA', 'GROUP'] and only_mesh:
|
|
return []
|
|
if self.type == 'LIGHT_DATA' and no_lights:
|
|
return []
|
|
if only_groups and self.type != 'GROUP':
|
|
return []
|
|
# Need to get all the grouped objects
|
|
if self.type == 'GROUP':
|
|
filter = list(self.object_types)
|
|
if no_lights:
|
|
filter.remove('LIGHT')
|
|
if only_mesh:
|
|
filter = ['MESH']
|
|
# Iterate over the objects applying the type filter
|
|
for obj in self.get_grouped():
|
|
if obj.type in filter:
|
|
objects.append([obj])
|
|
if only_groups:
|
|
return [[self.value, objects]]
|
|
# Mesh data can have a few extra properties
|
|
elif self.type == 'MESH_DATA':
|
|
uv_map = ""
|
|
if self.pick_uv and self.uv_map:
|
|
uv_map = self.uv_map
|
|
cage = None
|
|
if self.use_cage and self.cage:
|
|
cage = self.cage
|
|
objects.append([self.value, uv_map, cage])
|
|
else:
|
|
objects.append([self.value])
|
|
return objects
|
|
|
|
# Return objects contained in a group
|
|
def get_grouped(self):
|
|
if self.recursive:
|
|
return self.value.all_objects
|
|
else:
|
|
return self.value.objects
|
|
|
|
# Validate value(s)
|
|
def validate(self, check_materials=False, check_as_active=False, check_multi=False):
|
|
valid = [True]
|
|
# Follow links
|
|
if self.islinked() and self.valid:
|
|
return follow_input_link(self.links[0]).from_node.validate(check_materials, check_as_active, check_multi)
|
|
# Has a value and isn't linked
|
|
if self.value and self.type and not self.islinked():
|
|
objs = [self.value]
|
|
if self.type == 'GROUP':
|
|
objs = self.get_grouped()
|
|
|
|
# Iterate over objs, it will just be one object unless the type is group (but maintains a single algo for both)
|
|
for obj in objs:
|
|
# Perform checks needed for an active bake target
|
|
if check_as_active:
|
|
# Only a mesh type object can be a valid target, it will just be silently ignored
|
|
if obj.type != 'MESH':
|
|
return valid
|
|
# Any UV map?
|
|
if len(obj.data.uv_layers) < 1:
|
|
valid[0] = False
|
|
valid.append([_print("UV error", node=self.node, ret=True), ": No UV Maps found on object <%s>." % (obj.name)])
|
|
# Custom UV map still exists? (can't be done for grouped objects)
|
|
if self.type != 'GROUP' and self.pick_uv and self.uv_map not in obj.data.uv_layers and self.uv_map != "":
|
|
valid[0] = False
|
|
valid.append([_print("UV error", node=self.node, ret=True), ": Selected UV map <%s> not present on object <%s> (it could have been deleted or renamed)" % (self.uv_map, obj.name)])
|
|
# Check for a valid multi-res mod if enabled
|
|
if check_multi:
|
|
has_multi_mod = False
|
|
if len(obj.modifiers):
|
|
for mod in obj.modifiers:
|
|
if mod.type == 'MULTIRES' and mod.total_levels > 0:
|
|
has_multi_mod = True
|
|
break
|
|
if not has_multi_mod:
|
|
valid[0] = False
|
|
valid.append([_print("Multires error", node=self.node, ret=True), ": No multires data on object <%s>." % (obj.name)])
|
|
# Check that materials can be converted to enable PBR data bakes
|
|
if check_materials and obj.type in self.object_types:
|
|
mats = []
|
|
if len(obj.material_slots):
|
|
for slot in obj.material_slots:
|
|
mat = slot.material
|
|
if mat != None and not mat in mats:
|
|
mats.append(mat)
|
|
# Is node based?
|
|
if not mat.node_tree or not mat.node_tree.nodes:
|
|
valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> not a node based material" % (mat.name)])
|
|
continue
|
|
# Is a 'principled' material?
|
|
passed = False
|
|
for node in mat.node_tree.nodes:
|
|
if node.type == 'OUTPUT_MATERIAL' and node.target in ['CYCLES', 'ALL']:
|
|
if material_recursor(node):
|
|
passed = True
|
|
break
|
|
if not passed:
|
|
valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> Output doesn't appear to be a valid combination of Principled and Mix/Add shaders. Baked values may not be correct for this material." % (mat.name)])
|
|
return valid
|
|
|
|
# Blender Properties
|
|
value: bpy.props.PointerProperty(name="Object", description="Object to be used in some way in a bake pass", type=bpy.types.ID, poll=value_prop_filter, update=value_prop_update)
|
|
type: bpy.props.StringProperty(name="Type", description="ID String of value type", default="NONE")
|
|
recursive: bpy.props.BoolProperty(name="Recursive Selection", description="When enabled all collections within the selected collection will be used", default=False)
|
|
pick_uv: bpy.props.BoolProperty(name="Pick UV Map", description="Enables selecting which UV map to use instead of the active one", default=False)
|
|
uv_map: bpy.props.StringProperty(name="UV Map", description="UV Map to use instead of active if value is a mesh", default="")
|
|
use_cage: bpy.props.BoolProperty(name="Use Cage", description="Enables cage usage and selection of cage mesh", default=False, update=use_cage_update)
|
|
cage: bpy.props.PointerProperty(name="Cage", description="Mesh to use a cage", type=bpy.types.Object, poll=cage_prop_filter)
|
|
|
|
def draw(self, context, layout, node, text):
|
|
if not self.is_output and not self.islinked() and not self.node.bl_idname == 'BakeWrangler_Bake_Material':
|
|
row = layout.row(align=True)
|
|
label = ""
|
|
if self.name in ['Target', 'Source', 'Scene']:
|
|
split_fac = 44 / self.node.width
|
|
split = row.split(factor=split_fac)
|
|
rowl = split.row(align=True)
|
|
rowl.label(text=self.name)
|
|
row = split.row(align=True)
|
|
if self.name in ['Target', 'Source'] or (hasattr(node, "filter_collection") and not node.filter_collection):
|
|
row.prop_search(self, "value", bpy.data, "objects", text=label, icon=self.type)
|
|
else:
|
|
row.prop_search(self, "value", bpy.data, "collections", text=label, icon=self.type)
|
|
if self.value and self.type:
|
|
if self.type == 'GROUP':
|
|
row.prop(self, "recursive", icon='OUTLINER', text="")
|
|
if self.type == 'MESH_DATA':
|
|
row.prop(self, "pick_uv", icon='UV', text="")
|
|
if self.pick_uv:
|
|
row.prop_search(self, "uv_map", self.value.data, "uv_layers", text="", icon='UV_DATA')
|
|
row.prop(self, "use_cage", icon='FILE_VOLUME', text="")
|
|
if self.use_cage:
|
|
row.prop_search(self, "cage", bpy.data, "objects", text="", icon='MESH_DATA')
|
|
elif self.is_output:
|
|
row = layout.row(align=False)
|
|
row0 = row.row()
|
|
row0.ui_units_x = 50
|
|
op = row0.operator("bake_wrangler.add_selected")
|
|
op.tree = self.node.id_data.name
|
|
op.node = self.node.name
|
|
row2 = row.row(align=False)
|
|
row2.alignment = 'RIGHT'
|
|
row2.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
else:
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.2, 1.0, 1.0))
|
|
|
|
|
|
# Socket for materials baking
|
|
class BakeWrangler_Socket_Material(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for specifying a material to bake'''
|
|
bl_label = 'Material'
|
|
|
|
# Called when the value property changes
|
|
def value_prop_update(self, context):
|
|
if self.node:
|
|
self.node.update_inputs()
|
|
|
|
value: bpy.props.PointerProperty(name="Material", description="Material to be used in a bake pass", type=bpy.types.Material, update=value_prop_update)
|
|
|
|
def draw(self, context, layout, node, text):
|
|
if not self.is_output:
|
|
row = layout.row(align=True)
|
|
split_fac = 52 / self.node.width
|
|
split = row.split(factor=split_fac)
|
|
rowl = split.row(align=True)
|
|
rowl.label(text=self.name)
|
|
rowr = split.row(align=True)
|
|
rowr.prop_search(self, "value", bpy.data, "materials", icon='MATERIAL_DATA', text="")
|
|
else:
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
if self.is_output:
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0))
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.0, 0.0, 0.0))
|
|
|
|
|
|
# Socket for sharing a target mesh
|
|
class BakeWrangler_Socket_Mesh(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for connecting a mesh node'''
|
|
bl_label = 'Mesh'
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
return [self.bl_idname, 'BakeWrangler_Socket_Material']
|
|
|
|
def draw(self, context, layout, node, text):
|
|
if node.bl_idname == 'BakeWrangler_Bake_Pass':
|
|
if self.islinked() and self.valid:
|
|
if get_input(self).bl_idname == 'BakeWrangler_Bake_Material':
|
|
label = "Material"
|
|
else:
|
|
label = "Mesh"
|
|
else:
|
|
label = "Mesh / Material"
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, label))
|
|
else:
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0))
|
|
|
|
|
|
# Socket for RGB(A) data, extends the base color node
|
|
class BakeWrangler_Socket_Color(NodeSocketColor, BakeWrangler_Tree_Socket):
|
|
'''Socket for RGB(A) data'''
|
|
bl_label = 'Color'
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
return [self.bl_idname, 'BakeWrangler_Socket_Float']
|
|
|
|
#Props
|
|
suffix: bpy.props.StringProperty(name="Suffix", description="Suffix appended to filename for this output")
|
|
value_rgb: bpy.props.FloatVectorProperty(name="Color", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.5,0.5,0.5,1.0], size=4)
|
|
|
|
def draw(self, context, layout, node, text):
|
|
if self.is_linked and self.valid and node.bl_idname in ['BakeWrangler_Output_Image_Path', 'BakeWrangler_Output_Vertex_Cols']:
|
|
row = layout.row(align=True)
|
|
sfac = 40 / node.width
|
|
split = row.split(factor=sfac)
|
|
if node.bl_idname == 'BakeWrangler_Output_Image_Path':
|
|
split.label(text="Suffix")
|
|
else:
|
|
split.label(text="Name")
|
|
srow = split.row(align=True)
|
|
srow.prop(self, "suffix", text="")
|
|
idx = 0
|
|
for input in node.inputs:
|
|
if input == self:
|
|
break
|
|
idx += 1
|
|
BakeWrangler_Tree_Node.draw_bake_button(node, srow, 'IMAGE', "", True, idx)
|
|
elif node.bl_idname == 'BakeWrangler_Post_MixRGB' and not self.is_linked and not self.is_output:
|
|
layout.prop(self, "value_rgb", text=self.name)
|
|
else:
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.78, 0.78, 0.16, 1.0))
|
|
|
|
|
|
# Socket to map input value to output channel. Works like a separator/combiner node pair
|
|
class BakeWrangler_Socket_ChanMap(NodeSocketColor, BakeWrangler_Tree_Socket):
|
|
'''Socket for splitting and joining color channels'''
|
|
bl_label = 'Channel Map'
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
return ['BakeWrangler_Socket_Color']
|
|
|
|
channels = (
|
|
('Red', "Red", "Red"),
|
|
('Green', "Green", "Green"),
|
|
('Blue', "Blue", "Blue"),
|
|
('Value', "Value", "Value"),
|
|
)
|
|
|
|
# Props
|
|
input_channel: bpy.props.EnumProperty(name="Input Channel", description="Channel of input color data to take values from", items=channels, default='Value')
|
|
|
|
def draw(self, context, layout, node, text):
|
|
row = layout.row(align=True)
|
|
label = row.row()
|
|
if not self.is_output and self.is_linked and self.valid:
|
|
chan = row.row()
|
|
chan.prop(self, "input_channel", text="From")
|
|
label.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
if self.name == 'Alpha':
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.631, 0.631, 0.631, 1.0))
|
|
else:
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.78, 0.78, 0.16, 1.0))
|
|
|
|
|
|
# Socket for Float data, extends the base float node
|
|
class BakeWrangler_Socket_Float(NodeSocketFloat, BakeWrangler_Tree_Socket):
|
|
'''Socket for Float data'''
|
|
bl_label = 'Float'
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
return [self.bl_idname, 'BakeWrangler_Socket_Color']
|
|
|
|
value_fac: bpy.props.FloatProperty(name="Fac", default=0.5, subtype='FACTOR', min=0.0, max=1.0, precision=3, step=10)
|
|
value_col: bpy.props.FloatProperty(name="Fac", soft_min=0.0, soft_max=1.0, precision=3, step=10)
|
|
value_gam: bpy.props.FloatProperty(name="Fac", default=1.0, min=0.0, soft_min=0.001, soft_max=10.0, precision=3, step=1)
|
|
value: bpy.props.FloatProperty(name="Value", precision=3, step=10)
|
|
|
|
def draw(self, context, layout, node, text):
|
|
if node.bl_idname == 'BakeWrangler_Post_MixRGB' and not self.islinked() and not self.is_output:
|
|
layout.prop(self, "value_fac")
|
|
elif node.bl_idname == 'BakeWrangler_Post_JoinRGB' and not self.islinked() and not self.is_output:
|
|
layout.prop(self, "value_col", text=self.name)
|
|
elif node.bl_idname == 'BakeWrangler_Post_Math' and not self.islinked() and not self.is_output:
|
|
if node.op == 'POWER':
|
|
if self.identifier == '0':
|
|
layout.prop(self, "value", text="Base")
|
|
elif self.identifier == '1':
|
|
layout.prop(self, "value", text="Exponent")
|
|
elif node.op == 'LOGARITHM' and self.identifier == '1':
|
|
layout.prop(self, "value", text="Base")
|
|
else:
|
|
layout.prop(self, "value", text=self.name)
|
|
elif node.bl_idname == 'BakeWrangler_Post_Gamma' and not self.islinked() and not self.is_output:
|
|
layout.prop(self, "value_gam", text=self.name)
|
|
else:
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.631, 0.631, 0.631, 1.0))
|
|
|
|
|
|
# Socket for connecting an output image to a batch job node
|
|
class BakeWrangler_Socket_Bake(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for connecting an output image node to a batch node'''
|
|
bl_label = 'Bake'
|
|
|
|
def draw(self, context, layout, node, text):
|
|
row = layout.row(align=True)
|
|
if self.is_output:
|
|
row0 = row.row()
|
|
row0.ui_units_x = 50
|
|
row1 = row.row(align=False)
|
|
row1.alignment = 'RIGHT'
|
|
if self.node.bl_idname == 'BakeWrangler_Output_Image_Path':
|
|
BakeWrangler_Tree_Node.draw_bake_button(self.node, row0, 'IMAGE', "Bake Image")
|
|
elif self.node.bl_idname == 'BakeWrangler_Output_Vertex_Cols':
|
|
BakeWrangler_Tree_Node.draw_bake_button(self.node, row0, 'IMAGE', "Bake Colors")
|
|
row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
else:
|
|
row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (1.0, 0.5, 1.0, 1.0))
|
|
|
|
|
|
# Socket for connecting a mesh settings node to a mesh
|
|
class BakeWrangler_Socket_MeshSetting(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for connecting a mesh settings node to a mesh node'''
|
|
bl_label = 'Mesh Settings'
|
|
|
|
def draw(self, context, layout, node, text):
|
|
row = layout.row(align=False)
|
|
if self.is_output:
|
|
row0 = row.row()
|
|
row1 = row.row(align=False)
|
|
row1.alignment = 'RIGHT'
|
|
row1.ui_units_x = 100
|
|
if self.node.pinned:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
|
|
else:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
|
|
row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
else:
|
|
row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (1.0, 0.3, 0.0, 1.0))
|
|
|
|
|
|
# Socket for connecting a pass settings node to a pass
|
|
class BakeWrangler_Socket_PassSetting(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for connecting a pass settings node to a pass node'''
|
|
bl_label = 'Pass Settings'
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
return [self.bl_idname, 'BakeWrangler_Socket_SampleSetting']
|
|
|
|
def draw(self, context, layout, node, text):
|
|
row = layout.row(align=False)
|
|
if self.is_output:
|
|
row0 = row.row()
|
|
row1 = row.row(align=False)
|
|
row1.alignment = 'RIGHT'
|
|
row1.ui_units_x = 100
|
|
if self.node.pinned:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
|
|
else:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
|
|
row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
elif not self.is_linked:
|
|
split = row.split(factor=0.35)
|
|
split.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
split.prop(self.node, "bake_samples", text="Samples")
|
|
else:
|
|
row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.37, 0.08, 1.0, 1.0))
|
|
|
|
|
|
# Socket for connecting a pass settings node to a pass
|
|
class BakeWrangler_Socket_SampleSetting(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for connecting a pass settings node to a pass node'''
|
|
bl_label = 'Sample Settings'
|
|
|
|
def draw(self, context, layout, node, text):
|
|
row = layout.row(align=False)
|
|
if self.is_output:
|
|
row0 = row.row()
|
|
row1 = row.row(align=False)
|
|
row1.alignment = 'RIGHT'
|
|
row1.ui_units_x = 100
|
|
if self.node.pinned:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
|
|
else:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
|
|
row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
else:
|
|
row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.4, 0.08, 1.0, 1.0))
|
|
|
|
|
|
# Socket for connecting an output settings node to an output
|
|
class BakeWrangler_Socket_OutputSetting(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Socket for connecting an output settings node to an output node'''
|
|
bl_label = 'Output Settings'
|
|
|
|
def draw(self, context, layout, node, text):
|
|
row = layout.row(align=False)
|
|
if self.is_output:
|
|
row0 = row.row()
|
|
row1 = row.row(align=False)
|
|
row1.alignment = 'RIGHT'
|
|
row1.ui_units_x = 100
|
|
if self.node.pinned:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="PINNED", text="")
|
|
else:
|
|
row0.prop(self.node, "pinned", toggle=True, icon="UNPINNED", text="")
|
|
row1.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
else:
|
|
row.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.14, 1.0, 1.0, 1.0))
|
|
|
|
|
|
# Socket that takes the names of all objects input to use for filename outputs
|
|
class BakeWrangler_Socket_ObjectNames(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Take the names of input objects'''
|
|
bl_label = 'Object Names'
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
return ['BakeWrangler_Socket_Mesh', 'BakeWrangler_Socket_Object', 'BakeWrangler_Socket_Material']
|
|
|
|
def draw(self, context, layout, node, text):
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.5, 1.0, 1.0))
|
|
|
|
|
|
# Socket to take objects to use for splitting output into separate files
|
|
class BakeWrangler_Socket_SplitOutput(BakeWrangler_Tree_Socket, NodeSocket):
|
|
'''Split output into files based on input'''
|
|
bl_label = 'Split Output'
|
|
|
|
# Returns a list of valid bl_idnames that can connect
|
|
def valid_inputs(self):
|
|
if not self.name == "Path/Filename":
|
|
return [self.bl_idname, 'BakeWrangler_Socket_Mesh', 'BakeWrangler_Socket_Object', 'BakeWrangler_Socket_Material']
|
|
return [None]
|
|
|
|
# Get what ever the split list input node is, if there is one
|
|
def get_split(self):
|
|
linked = get_input(self)
|
|
if linked:
|
|
if linked.bl_idname == 'BakeWrangler_Input_Filenames':
|
|
return linked.get_names()
|
|
return [linked]
|
|
return None
|
|
|
|
# Get full path, removing any relative references
|
|
def get_full_path(self):
|
|
linked = get_input(self)
|
|
if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
|
|
return linked.inputs["Path/Filename"].get_full_path()
|
|
cwd = os.path.dirname(bpy.data.filepath)
|
|
self.img_path = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.disp_path)))
|
|
return self.img_path
|
|
|
|
# Deal with any path components that may be in the filename and remove recognised extensions
|
|
def update_filename(self, context):
|
|
if self.img_name == "":
|
|
return
|
|
fullpath = os.path.normpath(bpy.path.abspath(self.img_name))
|
|
path, name = os.path.split(fullpath)
|
|
if path:
|
|
self.disp_path = self.img_name[:-len(name)]
|
|
if name:
|
|
# Remove file extension if recognised
|
|
nname, ext = os.path.splitext(name)
|
|
if ext not in [".", "", None]:
|
|
for enum, iext in self.img_ext:
|
|
if ext.lower() == iext:
|
|
name = nname
|
|
break
|
|
if self.img_name != name:
|
|
self.img_name = name
|
|
|
|
# Return the file name with the correct image type extension (unless it has an existing unknown extension)
|
|
def name_with_ext(self, suffix="", type=""):
|
|
linked = get_input(self)
|
|
if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
|
|
return linked.inputs["Path/Filename"].name_with_ext(suffix, type)
|
|
for enum, iext in self.img_ext:
|
|
if type == enum:
|
|
return (self.img_name + suffix + iext)
|
|
|
|
def frame_range(self, padding=False, animated=False):
|
|
linked = get_input(self)
|
|
if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
|
|
return linked.get_frames(padding, animated)
|
|
if padding: return None
|
|
elif animated: return False
|
|
else: return []
|
|
|
|
def get_path(self):
|
|
linked = get_input(self)
|
|
if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
|
|
return linked.inputs["Path/Filename"].get_path()
|
|
return self.disp_path
|
|
|
|
def get_name(self):
|
|
linked = get_input(self)
|
|
if linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
|
|
return linked.inputs["Path/Filename"].get_name()
|
|
return self.img_name
|
|
|
|
# Properties
|
|
disp_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH')
|
|
img_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH')
|
|
img_name: bpy.props.StringProperty(name="Output File", description="File prefix to save image as", default="Image", subtype='FILE_PATH', update=update_filename)
|
|
|
|
img_ext = (
|
|
('BMP', ".bmp"),
|
|
('IRIS', ".rgb"),
|
|
('PNG', ".png"),
|
|
('JPEG', ".jpg"),
|
|
('JPEG2000', ".jp2"),
|
|
('TARGA', ".tga"),
|
|
('TARGA_RAW', ".tga"),
|
|
('CINEON', ".cin"),
|
|
('DPX', ".dpx"),
|
|
('OPEN_EXR_MULTILAYER', ".exr"),
|
|
('OPEN_EXR', ".exr"),
|
|
('HDR', ".hdr"),
|
|
('TIFF', ".tif"),
|
|
)
|
|
|
|
def draw(self, context, layout, node, text):
|
|
colpath = layout.column(align=True)
|
|
linked = get_input(self)
|
|
if node.bl_idname == 'BakeWrangler_Output_Image_Path' and linked and linked.bl_idname == 'BakeWrangler_Input_Filenames':
|
|
colpath.label(text="Path: " + linked.inputs["Path/Filename"].get_path())
|
|
colpath.label(text="Name: " + linked.inputs["Path/Filename"].get_name())
|
|
elif not self.is_output:
|
|
colpath.prop(self, "disp_path", text="")
|
|
colpath.prop(self, "img_name", text="")
|
|
else:
|
|
layout.label(text=BakeWrangler_Tree_Socket.socket_label(self, text))
|
|
|
|
def draw_color(self, context, node):
|
|
if self.node.bl_idname == 'BakeWrangler_Input_Filenames' and not self.is_output:
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.0, 0.0, 0.0))
|
|
return BakeWrangler_Tree_Socket.socket_color(self, (0.0, 0.35, 1.0, 1.0))
|
|
|
|
|
|
# Custom Nodes:
|
|
|
|
# Base class for all bakery nodes. Identifies that they belong in the bakery tree.
|
|
class BakeWrangler_Tree_Node:
|
|
# Tree version that created the node
|
|
tree_version: bpy.props.IntProperty(name="Tree Version", default=0)
|
|
def init(self, context):
|
|
self.tree_version = BW_TREE_VERSION
|
|
|
|
@classmethod
|
|
def poll(cls, ntree):
|
|
return ntree.bl_idname == 'BakeWrangler_Tree'
|
|
|
|
def get_name(self):
|
|
name = self.name
|
|
#if self.label:
|
|
# name += ".%s" % (self.label)
|
|
return name
|
|
|
|
def validate(self, inputs=False, endl=False):
|
|
if not inputs:
|
|
return [True]
|
|
valid = [True]
|
|
# Validate inputs
|
|
has_valid_input = False
|
|
for input in self.inputs:
|
|
if input.islinked() and input.valid:
|
|
input_valid = get_input(input).validate()
|
|
valid[0] = input_valid.pop(0)
|
|
if valid[0]:
|
|
has_valid_input = True
|
|
valid += input_valid
|
|
errs = len(valid)
|
|
if not has_valid_input and errs < 2:
|
|
if endl: return valid
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
|
|
return valid
|
|
|
|
# Makes sure there is always one empty input socket at the bottom by adding and removing sockets
|
|
def update_inputs(self, socket_type=None, socket_name=None, sub_socket_dict=None):
|
|
if socket_type is None or self.tree_version != BW_TREE_VERSION:
|
|
return
|
|
idx = 0
|
|
sub = 0
|
|
if sub_socket_dict:
|
|
sub = len(sub_socket_dict.keys())
|
|
for socket in self.inputs:
|
|
if socket.bl_idname != socket_type:
|
|
idx = idx + 1
|
|
continue
|
|
if socket.is_linked or (hasattr(socket, 'value') and socket.value):
|
|
if len(self.inputs) == idx + 1 + sub:
|
|
self.inputs.new(socket_type, socket_name)
|
|
if sub_socket_dict:
|
|
for key in sub_socket_dict.keys():
|
|
self.inputs.new(sub_socket_dict[key], key)
|
|
else:
|
|
if len(self.inputs) > idx + 1 + sub:
|
|
self.inputs.remove(socket)
|
|
rem = idx
|
|
idx = idx - 1
|
|
if sub_socket_dict:
|
|
for key in sub_socket_dict.keys():
|
|
self.inputs.remove(self.inputs[rem])
|
|
idx = idx - 1
|
|
idx = idx + 1
|
|
|
|
# Update inputs and links on updates
|
|
def update(self):
|
|
if self.tree_version != BW_TREE_VERSION:
|
|
return
|
|
self.update_inputs()
|
|
# Links can get inserted without calling insert_link, but update is called.
|
|
for socket in self.inputs:
|
|
if socket.islinked():
|
|
self.insert_link(socket.links[0])
|
|
|
|
# Validate incoming links
|
|
def insert_link(self, link):
|
|
if link.to_node == self:
|
|
if follow_input_link(link).from_socket.bl_idname in link.to_socket.valid_inputs() and link.is_valid:
|
|
link.to_socket.valid = True
|
|
else:
|
|
link.to_socket.valid = False
|
|
|
|
# Draw a double/halve button set
|
|
def draw_double_halve(self, layout, value):
|
|
op = layout.operator("bake_wrangler.double_val", icon='SORT_DESC', text="")
|
|
BakeWrangler_Operator_DoubleVal.set_props(op, self.name, self.id_data.name, value)
|
|
op = layout.operator("bake_wrangler.double_val", icon='SORT_ASC', text="")
|
|
BakeWrangler_Operator_DoubleVal.set_props(op, self.name, self.id_data.name, value, True)
|
|
|
|
# Draw bake button in correct state
|
|
def draw_bake_button(self, layout, icon, label, icon_only=False, socket=-1):
|
|
is_self = False
|
|
baking_valid = False
|
|
try:
|
|
if self.id_data.baking:
|
|
baking_valid = True
|
|
if self.id_data.baking.node == self.name:
|
|
is_self = True
|
|
except ReferenceError:
|
|
is_self = False
|
|
baking_valid = False
|
|
|
|
if baking_valid:
|
|
if is_self:
|
|
if self.id_data.baking.stop(kill=False):
|
|
if icon_only:
|
|
stext = ""
|
|
else:
|
|
stext = "Stopping..."
|
|
layout.operator("bake_wrangler.dummy", icon='CANCEL', text=stext)
|
|
else:
|
|
if icon_only:
|
|
stext = ""
|
|
else:
|
|
stext = "Cancel Bake"
|
|
op = layout.operator("bake_wrangler.bake_stop", icon='CANCEL', text=stext)
|
|
op.tree = self.id_data.name
|
|
op.node = self.name
|
|
op.sock = socket
|
|
else:
|
|
layout.operator("bake_wrangler.dummy", icon=icon, text=label)
|
|
else:
|
|
op = layout.operator("bake_wrangler.bake_pass", icon=icon, text=label)
|
|
op.tree = self.id_data.name
|
|
op.node = self.name
|
|
op.sock = socket
|
|
|
|
|
|
# All settings which are not pass specific
|
|
class BakeWrangler_Settings(BakeWrangler_Tree_Node, Node):
|
|
'''Settings node'''
|
|
bl_label = 'Settings'
|
|
bl_width_default = 173
|
|
|
|
# Inputs are static (none)
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
# Only one of this node can be pinned at a time
|
|
def pin_node(self, context):
|
|
if self.pinned:
|
|
tree = self.id_data
|
|
for node in tree.nodes:
|
|
if node != self and node.bl_idname == 'BakeWrangler_Settings':
|
|
node.pinned = False
|
|
|
|
# Props
|
|
pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
|
|
ray_dist: bpy.props.FloatProperty(name="Ray Distance", description="Distance to use for inward ray cast when using a selected to active bake", default=0.01, step=1, min=0.0, unit='LENGTH')
|
|
max_ray_dist: bpy.props.FloatProperty(name="Max Ray Dist", description="The maximum ray distance for matching points between the active and selected objects. If zero, there is no limit", default=0.0, step=1, min=0.0, unit='LENGTH')
|
|
margin: bpy.props.IntProperty(name="Margin", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
|
|
margin_extend: bpy.props.BoolProperty(name="Extend", description="Margin extends border pixels outward (instead of taking values from adjacent faces)", default=True)
|
|
#mask_margin: bpy.props.IntProperty(name="Mask Margin", description="Adds extra padding to the mask bake. Use if edge details are being cut off when masking is enabled", default=0, min=0, subtype='PIXEL')
|
|
auto_cage: bpy.props.BoolProperty(name="Auto Cage", description="Automatically generate a cage for objects that don't have one set", default=False)
|
|
acage_expansion: bpy.props.FloatProperty(name="Cage Expansion", description="Distance to expand automatically generated cage geometry from original object", default=0.02, step=0.01, precision=3, unit='LENGTH')
|
|
acage_smooth: bpy.props.IntProperty(name="Cage Smoothing Angle", description="Angle range that automatic normal smoothing will be applied to", default=179)
|
|
material_replace: bpy.props.BoolProperty(name="Material Override", description="Replace all materials on selected objects with the specified material (objects without a material will have it added)", default=False)
|
|
material_override: bpy.props.PointerProperty(name="Override Material", description="Material that will be used in place of all other materials", type=bpy.types.Material)
|
|
material_osl: bpy.props.BoolProperty(name="OSL", description="Material uses an OSL shader node", default=False)
|
|
bake_mods: bpy.props.BoolProperty(name="Bake Mods to Unmodded", description="Modifiers with viewport visibility enabled will be stripped from Target objects and a copy with those modifiers applied will be created and used as the Source object (Disable viewport visibility on a modifier to exclude it - this setting can be inverted in the add-on preferences if you prefer the visibilty setting to work the other way)", default=False)
|
|
|
|
cycles_devices = (
|
|
('CPU', "CPU", "Use CPU for baking"),
|
|
('GPU', "GPU", "Use GPU for baking"),
|
|
)
|
|
|
|
tile_sizes = (
|
|
('DEF', "Default", "Use Bake Wrangler default"),
|
|
('IMG', "Bake Size", "Use size of bake as tile size"),
|
|
('CUST', "Custom", "Enter your own custom tile size"),
|
|
)
|
|
|
|
# Props
|
|
pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
|
|
res_bake_x: bpy.props.IntProperty(name="Bake X resolution ", description="Width (X) to bake maps at", default=2048, min=1, subtype='PIXEL')
|
|
res_bake_y: bpy.props.IntProperty(name="Bake Y resolution ", description="Height (Y) to bake maps at", default=2048, min=1, subtype='PIXEL')
|
|
bake_device: bpy.props.EnumProperty(name="Device", description="Bake device", items=cycles_devices, default='CPU')
|
|
interpolate: bpy.props.BoolProperty(name="Interpolate", description="Use cubic interpolation between baked pixel and output pixel, creating a soft anti-aliasing effect", default=False)
|
|
|
|
adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False)
|
|
use_world: bpy.props.BoolProperty(name="Use World", description="Enabled to pick a world to use (empty to use active), instead of Bake Wranglers default", default=False)
|
|
the_world: bpy.props.PointerProperty(name="World", description="World to use instead of Bake Wranglers default (empty to use active)", type=bpy.types.World)
|
|
cpy_render: bpy.props.BoolProperty(name="Copy Settings", description="Copy render settings from selected scene (empty to use active), instead of using defaults", default=False)
|
|
cpy_from: bpy.props.PointerProperty(name="Render Scene", description="Scene to copy render settings from (empty to use active)", type=bpy.types.Scene)
|
|
render_tile: bpy.props.IntProperty(name="Tiles", description="Render tile size", default=2048, min=8, subtype='PIXEL')
|
|
use_tiles: bpy.props.EnumProperty(name="Tiles", description="Render tile size", items=tile_sizes, default='DEF')
|
|
render_threads: bpy.props.IntProperty(name="Threads", description="Maximum number of CPU cores to use simultaneously (set to zero for automatic)", default=0, min=0, max=1024)
|
|
use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False)
|
|
bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4)
|
|
|
|
bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 1 for all PBR passes and Normal maps. Values over 50 generally won't improve results.\nQuality is gained by increasing resolution rather than samples past that point", default=1, min=1)
|
|
bake_threshold: bpy.props.FloatProperty(name="Noise Threshold", description="Noise level to stop sampling at if reached before sample count", default=0.01, min=0.001, max=1.0)
|
|
bake_usethresh: bpy.props.BoolProperty(name="Use Threshold", description="Enables use of noise level threshold", default=False)
|
|
bake_timelimit: bpy.props.FloatProperty(name="Time Limit", description="Maximum time to spend on a single bake. Zero to disable", default=0.0, min=0.0, subtype='TIME_ABSOLUTE', unit='TIME_ABSOLUTE', step=100)
|
|
|
|
# Update output nodes to display alpha input or not depending on setting
|
|
def check_alpha(self, context):
|
|
tree = self.id_data
|
|
for node in tree.nodes:
|
|
if node.bl_idname == 'BakeWrangler_Output_Image_Path':
|
|
node.update_inputs()
|
|
|
|
# Recreate image format drop down as the built in one doesn't seem usable? Also most of the settings
|
|
# for the built in image settings selector don't seem applicable to saving from script...
|
|
img_format = (
|
|
('BMP', "BMP", "Output image in bitmap format."),
|
|
('IRIS', "Iris", "Output image in (old!) SGI IRIS format."),
|
|
('PNG', "PNG", "Output image in PNG format."),
|
|
('JPEG', "JPEG", "Output image in JPEG format."),
|
|
('JPEG2000', "JPEG 2000", "Output image in JPEG 2000 format."),
|
|
('TARGA', "Targa", "Output image in Targa format."),
|
|
('TARGA_RAW', "Targa Raw", "Output image in uncompressed Targa format."),
|
|
('CINEON', "Cineon", "Output image in Cineon format."),
|
|
('DPX', "DPX", "Output image in DPX format."),
|
|
('OPEN_EXR_MULTILAYER', "OpenEXR MultiLayer", "Output image in multilayer OpenEXR format."),
|
|
('OPEN_EXR', "OpenEXR", "Output image in OpenEXR format."),
|
|
('HDR', "Radiance HDR", "Output image in Radiance HDR format."),
|
|
('TIFF', "TIFF", "Output image in TIFF format."),
|
|
)
|
|
|
|
img_color_modes = (
|
|
('BW', "BW", "Image saved in 8 bit grayscale"),
|
|
('RGB', "RGB", "Image saved with RGB (color) data"),
|
|
('RGBA', "RGBA", "Image saved with RGB and Alpha data"),
|
|
)
|
|
|
|
img_color_modes_noalpha = (
|
|
('BW', "BW", "Image saved in 8 bit grayscale"),
|
|
('RGB', "RGB", "Image saved with RGB (color) data"),
|
|
)
|
|
|
|
img_color_depths_8_16 = (
|
|
('8', "8", "8 bit color channels"),
|
|
('16', "16", "16 bit color channels"),
|
|
)
|
|
|
|
img_color_depths_8_12_16 = (
|
|
('8', "8", "8 bit color channels"),
|
|
('12', "12", "12 bit color channels"),
|
|
('16', "16", "16 bit color channels"),
|
|
)
|
|
|
|
img_color_depths_8_10_12_16 = (
|
|
('8', "8", "8 bit color channels"),
|
|
('10', "10", "10 bit color channels"),
|
|
('12', "12", "12 bit color channels"),
|
|
('16', "16", "16 bit color channels"),
|
|
)
|
|
|
|
img_color_depths_16_32 = (
|
|
('16', "Float (Half)", "16 bit color channels"),
|
|
('32', "Float (Full)", "32 bit color channels"),
|
|
)
|
|
|
|
img_codecs_jpeg2k = (
|
|
('JP2', "JP2", ""),
|
|
('J2K', "J2K", ""),
|
|
)
|
|
|
|
img_codecs_openexr = (
|
|
('DWAA', "DWAA (lossy)", ""),
|
|
('B44A', "B44A (lossy)", ""),
|
|
('ZIPS', "ZIPS (lossless)", ""),
|
|
('RLE', "RLE (lossless)", ""),
|
|
('RLE', "RLE (lossless)", ""),
|
|
('PIZ', "PIZ (lossless)", ""),
|
|
('ZIP', "ZIP (lossless)", ""),
|
|
('PXR24', "Pxr24 (lossy)", ""),
|
|
('NONE', "None", ""),
|
|
)
|
|
|
|
img_codecs_tiff = (
|
|
('PACKBITS', "Pack Bits", ""),
|
|
('LZW', "LZW", ""),
|
|
('DEFLATE', "Deflate", ""),
|
|
('NONE', "None", ""),
|
|
)
|
|
|
|
img_color_spaces = []
|
|
for space in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys():
|
|
img_color_spaces.append((space, space, space))
|
|
img_color_spaces = tuple(img_color_spaces)
|
|
|
|
# Props
|
|
pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
|
|
img_xres: bpy.props.IntProperty(name="Image X resolution", description="Number of horizontal pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
|
|
img_yres: bpy.props.IntProperty(name="Image Y resolution", description="Number of vertical pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
|
|
img_clear: bpy.props.BoolProperty(name="Clear Image", description="Clear image before writing bake data", default=False)
|
|
img_udim: bpy.props.BoolProperty(name="UDIM", description="Treat UV map as UDIM space and append standard number system to file name", default=False)
|
|
img_type: bpy.props.EnumProperty(name="Image Format", description="File format to save bake as", items=img_format, default='PNG')
|
|
fast_aa: bpy.props.BoolProperty(name="Fast Anti-Alias", description="Fast Anti-Aliasing. For more control use down or up sampling of bake to output by using different resolutions", default=False)
|
|
fast_aa_lvl: bpy.props.IntProperty(name="Fast AA Level", description="Level of fast AA to apply from 1 to 9", default=3, min=1, max=9)
|
|
|
|
marginer: bpy.props.BoolProperty(name="Marginer", description="Use alternative margin generator (slower)", default=False)
|
|
marginer_size: bpy.props.IntProperty(name="Marginer Size", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
|
|
marginer_fill: bpy.props.BoolProperty(name="Marginer Fill", description="Fill all gaps with margin instead of using a fixed width", default=False)
|
|
|
|
adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Display or hide advanced settings", default=False)
|
|
|
|
# Core settings
|
|
img_color_space: bpy.props.EnumProperty(name="Color Space", description="Color space to use when saving the image", items=img_color_spaces)
|
|
img_use_float: bpy.props.BoolProperty(name="Use 32 Bit Float", description="Generate all input passes using 32 bit floating point color (128 bits per pixel). Note this isn't very useful if your image format isn't set to a high bit depth", default=False)
|
|
img_color_mode: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels, and RGBA for saving red, green, blue and alpha channels", items=img_color_modes, default='RGB', update=check_alpha)
|
|
img_color_mode_noalpha: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels", items=img_color_modes_noalpha, default='RGB')
|
|
|
|
# Color Depths
|
|
img_color_depth_8_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_16, default='8')
|
|
img_color_depth_8_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_12_16, default='8')
|
|
img_color_depth_8_10_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_10_12_16, default='8')
|
|
img_color_depth_16_32: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_16_32, default='16')
|
|
|
|
# Compression / Quality
|
|
img_compression: bpy.props.IntProperty(name="Compression", description="Amount of time to determine best compression: 0 = no compression, 100 = maximum lossless compression", default=15, min=0, max=100, subtype='PERCENTAGE')
|
|
img_quality: bpy.props.IntProperty(name="Quality", description="Quality for image formats that support lossy compression", default=90, min=0, max=100, subtype='PERCENTAGE')
|
|
|
|
# Codecs
|
|
img_codec_jpeg2k: bpy.props.EnumProperty(name="Codec", description="Codec settings for jpeg2000", items=img_codecs_jpeg2k, default='JP2')
|
|
img_codec_openexr: bpy.props.EnumProperty(name="Codec", description="Codec settings for OpenEXR", items=img_codecs_openexr, default='ZIP')
|
|
img_codec_tiff: bpy.props.EnumProperty(name="Compression", description="Compression mode for TIFF", items=img_codecs_tiff, default='DEFLATE')
|
|
|
|
# Other random image format settings
|
|
img_jpeg2k_cinema: bpy.props.BoolProperty(name="Cinema", description="Use Openjpeg Cinema Preset", default=True)
|
|
img_jpeg2k_cinema48: bpy.props.BoolProperty(name="Cinema (48)", description="Use Openjpeg Cinema Preset (48 fps)", default=False)
|
|
img_jpeg2k_ycc: bpy.props.BoolProperty(name="YCC", description="Save luminance-chrominance-chrominance channels instead of RGB colors", default=False)
|
|
img_dpx_log: bpy.props.BoolProperty(name="Log", description="Convert to logarithmic color space", default=False)
|
|
img_openexr_zbuff: bpy.props.BoolProperty(name="Z Buffer", description="Save the z-depth per pixel (32 bit unsigned int z-buffer)", default=True)
|
|
|
|
def copy(self, node):
|
|
self.pinned = False
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN (none)
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_MeshSetting', "Mesh Settings")
|
|
# Prefs
|
|
self.ray_dist = _prefs("def_raydist")
|
|
self.max_ray_dist = _prefs("def_max_ray_dist")
|
|
self.margin = _prefs("def_margin")
|
|
#self.mask_margin = _prefs("def_mask_margin")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_PassSetting', "Pass Settings")
|
|
# Prefs
|
|
self.res_bake_x = _prefs("def_xres")
|
|
self.res_bake_y = _prefs("def_yres")
|
|
self.bake_samples = _prefs("def_samples")
|
|
self.bake_device = self.cycles_devices[int(_prefs("def_device"))][0]
|
|
self.adv_settings = _prefs("def_show_adv")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_SampleSetting', "Sample Settings")
|
|
# Prefs
|
|
self.bake_samples = _prefs("def_samples")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_OutputSetting', "Output Settings")
|
|
# Prefs
|
|
self.img_type = self.img_format[_prefs("def_format")][0]
|
|
self.img_xres = _prefs("def_xout")
|
|
self.img_yres = _prefs("def_yout")
|
|
self.adv_settings = _prefs("def_show_adv")
|
|
self.img_color_space = bpy.data.scenes[0].sequencer_colorspace_settings.name
|
|
|
|
def draw_buttons(self, context, layout):
|
|
col = layout.column(align=True)
|
|
row = col.row(align=True)
|
|
row.prop(self, "margin")
|
|
row.prop(self, "margin_extend", toggle=True, icon_only=True, icon='IMAGE_PLANE')
|
|
#col.prop(self, "mask_margin")
|
|
col.prop(self, "ray_dist")
|
|
col.prop(self, "max_ray_dist")
|
|
if not self.auto_cage:
|
|
col.prop(self, "auto_cage", toggle=True)
|
|
else:
|
|
row = col.row(align=True)
|
|
row.prop(self, "auto_cage", toggle=True)
|
|
row.prop(self, "acage_expansion", text="")
|
|
row.prop(self, "acage_smooth", text="")
|
|
if not self.material_replace:
|
|
col.prop(self, "material_replace", toggle=True)
|
|
else:
|
|
row = col.row(align=True)
|
|
row.prop(self, "material_replace", toggle=True)
|
|
row.prop_search(self, "material_override", bpy.data, "materials", text="")
|
|
row.prop(self, "material_osl", toggle=True, icon_only=True, icon='SCRIPT')
|
|
col.prop(self, "bake_mods", toggle=True)
|
|
|
|
colnode = layout.column(align=False)
|
|
colbasic = colnode.column(align=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "res_bake_x", text="X")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_x")
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "res_bake_y", text="Y")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_y")
|
|
#colbasic.prop(self, "bake_samples", text="Samples")
|
|
colbasic.prop(self, "interpolate")
|
|
|
|
split = colnode.split(factor=0.35)
|
|
split.label(text="Device:")
|
|
split.prop(self, "bake_device", text="")
|
|
|
|
advrow = colnode.row()
|
|
advrow.alignment = 'LEFT'
|
|
if not self.adv_settings:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
else:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
advcol = colnode.column(align=True)
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "use_world", text="Use My World", toggle=True)
|
|
if self.use_world:
|
|
row.prop_search(self, "the_world", bpy.data, "worlds", text="")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "cpy_render", text="Use My Settings", toggle=True)
|
|
if self.cpy_render:
|
|
row.prop_search(self, "cpy_from", bpy.data, "scenes", text="")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "use_tiles")
|
|
if self.use_tiles == 'CUST':
|
|
row.prop(self, "render_tile", text="")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "render_threads")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "use_bg_col", toggle=True)
|
|
if self.use_bg_col:
|
|
row.prop(self, "bg_color", text="")
|
|
|
|
colnode = layout.column(align=False)
|
|
colbasic = colnode.column(align=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.label(text="Noise")
|
|
rowbasic.prop(self, "bake_usethresh", text="")
|
|
rowbasic.prop(self, "bake_threshold", text="")
|
|
colbasic.prop(self, "bake_samples", text="Samples")
|
|
colbasic.prop(self, "bake_timelimit")
|
|
|
|
colnode = layout.column(align=False)
|
|
colbasic = colnode.column(align=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "img_xres", text="X")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_xres")
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "img_yres", text="Y")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_yres")
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "img_clear", text="Clear", toggle=True)
|
|
rowbasic.prop(self, "img_udim", toggle=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
if not self.fast_aa:
|
|
rowbasic.prop(self, "fast_aa", toggle=True)
|
|
if self.fast_aa:
|
|
rowbasic.prop(self, "fast_aa", toggle=True, text="Fast AA:")
|
|
rowbasic.prop(self, "fast_aa_lvl", text="")
|
|
|
|
split = colnode.split(factor=0.35)
|
|
split.label(text="Format:")
|
|
split.prop(self, "img_type", text="")
|
|
|
|
advrow = colnode.row()
|
|
advrow.alignment = 'LEFT'
|
|
if not self.adv_settings:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
else:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
coladv = colnode.column(align=True)
|
|
|
|
row = coladv.row(align=True)
|
|
if self.marginer:
|
|
row.prop(self, "marginer", toggle=True, icon_only=True, icon='NODE_INSERT_OFF')
|
|
else:
|
|
row.prop(self, "marginer", toggle=True, icon='NODE_INSERT_OFF')
|
|
if self.marginer:
|
|
row_size = row.row(align=True)
|
|
row_size.prop(self, "marginer_size", text="")
|
|
if self.marginer_fill:
|
|
row_size.enabled = False
|
|
row.prop(self, "marginer_fill", toggle=True, icon_only=True, icon='TPAINT_HLT')
|
|
|
|
coladv.prop(self, "img_use_float", toggle=True)
|
|
|
|
splitadv = coladv.split(factor=0.4)
|
|
coladvtxt = splitadv.column(align=True)
|
|
coladvopt = splitadv.column(align=True)
|
|
|
|
# Color Spaces
|
|
if self.img_type != 'CINEON':
|
|
coladvtxt.label(text="Space:")
|
|
coladvopt.prop(self, "img_color_space", text="")
|
|
# Color Modes
|
|
if self.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']:
|
|
coladvtxt.label(text="Color:")
|
|
coladvopt.prop(self, "img_color_mode_noalpha", text="")
|
|
if self.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']:
|
|
coladvtxt.label(text="Color:")
|
|
coladvopt.prop(self, "img_color_mode", text="")
|
|
# Color Depths
|
|
if self.img_type in ['PNG', 'TIFF']:
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_8_16", text="")
|
|
if self.img_type == 'JPEG2000':
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_8_12_16", text="")
|
|
if self.img_type == 'DPX':
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_8_10_12_16", text="")
|
|
if self.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']:
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_16_32", text="")
|
|
# Compression / Quality
|
|
if self.img_type == 'PNG':
|
|
coladvtxt.label(text="Compression:")
|
|
coladvopt.prop(self, "img_compression", text="")
|
|
if self.img_type in ['JPEG', 'JPEG2000']:
|
|
coladvtxt.label(text="Quality:")
|
|
coladvopt.prop(self, "img_quality", text="")
|
|
# Codecs
|
|
if self.img_type == 'JPEG2000':
|
|
coladvtxt.label(text="Codec:")
|
|
coladvopt.prop(self, "img_codec_jpeg2k", text="")
|
|
if self.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']:
|
|
coladvtxt.label(text="Codec:")
|
|
coladvopt.prop(self, "img_codec_openexr", text="")
|
|
if self.img_type == 'TIFF':
|
|
coladvtxt.label(text="Compression:")
|
|
coladvopt.prop(self, "img_codec_tiff", text="")
|
|
# Other random image settings
|
|
if self.img_type == 'JPEG2000':
|
|
coladv.prop(self, "img_jpeg2k_cinema")
|
|
coladv.prop(self, "img_jpeg2k_cinema48")
|
|
coladv.prop(self, "img_jpeg2k_ycc")
|
|
if self.img_type == 'DPX':
|
|
coladv.prop(self, "img_dpx_log")
|
|
if self.img_type == 'OPEN_EXR':
|
|
coladv.prop(self, "img_openexr_zbuff")
|
|
|
|
|
|
# Node to configure pass settings, which can be pinned as global
|
|
class BakeWrangler_MeshSettings(BakeWrangler_Tree_Node, Node):
|
|
'''Mesh settings node'''
|
|
bl_label = 'Mesh Settings'
|
|
bl_width_default = 173
|
|
|
|
# Inputs are static (none)
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
# Only one of this node can be pinned at a time
|
|
def pin_node(self, context):
|
|
if self.pinned:
|
|
tree = self.id_data
|
|
for node in tree.nodes:
|
|
if node != self and node.bl_idname == 'BakeWrangler_MeshSettings':
|
|
node.pinned = False
|
|
|
|
# Props
|
|
pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
|
|
ray_dist: bpy.props.FloatProperty(name="Ray Distance", description="Distance to use for inward ray cast when using a selected to active bake", default=0.01, step=1, min=0.0, unit='LENGTH')
|
|
max_ray_dist: bpy.props.FloatProperty(name="Max Ray Dist", description="The maximum ray distance for matching points between the active and selected objects. If zero, there is no limit", default=0.0, step=1, min=0.0, unit='LENGTH')
|
|
margin: bpy.props.IntProperty(name="Margin", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
|
|
margin_extend: bpy.props.BoolProperty(name="Extend", description="Margin extends border pixels outward (instead of taking values from adjacent faces)", default=True)
|
|
margin_auto: bpy.props.BoolProperty(name="Auto Margin", description="Automatically set margin size based on smallest dimension of texture", default=True)
|
|
#mask_margin: bpy.props.IntProperty(name="Mask Margin", description="Adds extra padding to the mask bake. Use if edge details are being cut off when masking is enabled", default=0, min=0, subtype='PIXEL')
|
|
auto_cage: bpy.props.BoolProperty(name="Auto Cage", description="Automatically generate a cage for objects that don't have one set", default=False)
|
|
acage_expansion: bpy.props.FloatProperty(name="Cage Expansion", description="Distance to expand automatically generated cage geometry from original object", default=0.02, step=0.01, precision=3, unit='LENGTH')
|
|
acage_smooth: bpy.props.IntProperty(name="Cage Smoothing Angle", description="Angle range that automatic normal smoothing will be applied to", default=179)
|
|
material_replace: bpy.props.BoolProperty(name="Material Override", description="Replace all materials on selected objects with the specified material (objects without a material will have it added)", default=False)
|
|
material_override: bpy.props.PointerProperty(name="Override Material", description="Material that will be used in place of all other materials", type=bpy.types.Material)
|
|
material_osl: bpy.props.BoolProperty(name="OSL", description="Material uses an OSL shader node", default=False)
|
|
bake_mods: bpy.props.BoolProperty(name="Bake Mods to Unmodded", description="Modifiers with viewport visibility enabled will be stripped from Target objects and a copy with those modifiers applied will be created and used as the Source object (Disable viewport visibility on a modifier to exclude it - this setting can be inverted in the add-on preferences if you prefer the visibilty setting to work the other way)", default=False)
|
|
|
|
def copy(self, node):
|
|
self.pinned = False
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN (none)
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_MeshSetting', "Mesh Settings")
|
|
# Prefs
|
|
self.ray_dist = _prefs("def_raydist")
|
|
self.max_ray_dist = _prefs("def_max_ray_dist")
|
|
self.margin = _prefs("def_margin")
|
|
#self.mask_margin = _prefs("def_mask_margin")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
col = layout.column(align=True)
|
|
row = col.row(align=True)
|
|
row.prop(self, "margin_auto", toggle=True, icon_only=True, icon='MOD_MESHDEFORM')
|
|
mrg = row.row(align=True)
|
|
mrg.prop(self, "margin")
|
|
if self.margin_auto:
|
|
mrg.enabled = False
|
|
row.prop(self, "margin_extend", toggle=True, icon_only=True, icon='IMAGE_PLANE')
|
|
#col.prop(self, "mask_margin")
|
|
col.prop(self, "ray_dist")
|
|
col.prop(self, "max_ray_dist")
|
|
if not self.auto_cage:
|
|
col.prop(self, "auto_cage", toggle=True)
|
|
else:
|
|
row = col.row(align=True)
|
|
row.prop(self, "auto_cage", toggle=True)
|
|
row.prop(self, "acage_expansion", text="")
|
|
row.prop(self, "acage_smooth", text="")
|
|
if not self.material_replace:
|
|
col.prop(self, "material_replace", toggle=True)
|
|
else:
|
|
row = col.row(align=True)
|
|
row.prop(self, "material_replace", toggle=True)
|
|
row.prop_search(self, "material_override", bpy.data, "materials", text="")
|
|
row.prop(self, "material_osl", toggle=True, icon_only=True, icon='SCRIPT')
|
|
col.prop(self, "bake_mods", toggle=True)
|
|
|
|
|
|
# Node to configure pass settings, which can be pinned as global
|
|
class BakeWrangler_PassSettings(BakeWrangler_Tree_Node, Node):
|
|
'''Pass settings node'''
|
|
bl_label = 'Pass Settings'
|
|
bl_width_default = 144
|
|
|
|
# Inputs are static (none)
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
# Only one of this node can be pinned at a time
|
|
def pin_node(self, context):
|
|
if self.pinned:
|
|
tree = self.id_data
|
|
for node in tree.nodes:
|
|
if node != self and node.bl_idname == 'BakeWrangler_PassSettings':
|
|
node.pinned = False
|
|
|
|
cycles_devices = (
|
|
('CPU', "CPU", "Use CPU for baking"),
|
|
('GPU', "GPU", "Use GPU for baking"),
|
|
)
|
|
|
|
tile_sizes = (
|
|
('DEF', "Default", "Use Bake Wrangler default"),
|
|
('IMG', "Bake Size", "Use size of bake as tile size"),
|
|
('CUST', "Custom", "Enter your own custom tile size"),
|
|
)
|
|
|
|
# Props
|
|
pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
|
|
res_bake_x: bpy.props.IntProperty(name="Bake X resolution ", description="Width (X) to bake maps at", default=2048, min=1, subtype='PIXEL')
|
|
res_bake_y: bpy.props.IntProperty(name="Bake Y resolution ", description="Height (Y) to bake maps at", default=2048, min=1, subtype='PIXEL')
|
|
bake_device: bpy.props.EnumProperty(name="Device", description="Bake device", items=cycles_devices, default='CPU')
|
|
interpolate: bpy.props.BoolProperty(name="Interpolate", description="Use cubic interpolation between baked pixel and output pixel, creating a soft anti-aliasing effect", default=False)
|
|
|
|
adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False)
|
|
use_world: bpy.props.BoolProperty(name="Use World", description="Enabled to pick a world to use (empty to use active), instead of Bake Wranglers default", default=False)
|
|
the_world: bpy.props.PointerProperty(name="World", description="World to use instead of Bake Wranglers default (empty to use active)", type=bpy.types.World)
|
|
cpy_render: bpy.props.BoolProperty(name="Copy Settings", description="Copy render settings from selected scene (empty to use active), instead of using defaults", default=False)
|
|
cpy_from: bpy.props.PointerProperty(name="Render Scene", description="Scene to copy render settings from (empty to use active)", type=bpy.types.Scene)
|
|
render_tile: bpy.props.IntProperty(name="Tiles", description="Render tile size", default=2048, min=8, subtype='PIXEL')
|
|
use_tiles: bpy.props.EnumProperty(name="Tiles", description="Render tile size", items=tile_sizes, default='DEF')
|
|
render_threads: bpy.props.IntProperty(name="Threads", description="Maximum number of CPU cores to use simultaneously (set to zero for automatic)", default=0, min=0, max=1024)
|
|
use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False)
|
|
bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4)
|
|
|
|
def copy(self, node):
|
|
self.pinned = False
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN (none)
|
|
self.inputs.new('BakeWrangler_Socket_SampleSetting', "Samples")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_PassSetting', "Pass Settings")
|
|
# Prefs
|
|
self.res_bake_x = _prefs("def_xres")
|
|
self.res_bake_y = _prefs("def_yres")
|
|
self.bake_samples = _prefs("def_samples")
|
|
self.bake_device = self.cycles_devices[int(_prefs("def_device"))][0]
|
|
self.adv_settings = _prefs("def_show_adv")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
colnode = layout.column(align=False)
|
|
|
|
colbasic = colnode.column(align=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "res_bake_x", text="X")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_x")
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "res_bake_y", text="Y")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "res_bake_y")
|
|
#colbasic.prop(self, "bake_samples", text="Samples")
|
|
colbasic.prop(self, "interpolate")
|
|
|
|
split = colnode.split(factor=0.35)
|
|
split.label(text="Device:")
|
|
split.prop(self, "bake_device", text="")
|
|
|
|
advrow = colnode.row()
|
|
advrow.alignment = 'LEFT'
|
|
if not self.adv_settings:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
else:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
advcol = colnode.column(align=True)
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "use_world", text="Use My World", toggle=True)
|
|
if self.use_world:
|
|
row.prop_search(self, "the_world", bpy.data, "worlds", text="")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "cpy_render", text="Use My Settings", toggle=True)
|
|
if self.cpy_render:
|
|
row.prop_search(self, "cpy_from", bpy.data, "scenes", text="")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "use_tiles")
|
|
if self.use_tiles == 'CUST':
|
|
row.prop(self, "render_tile", text="")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "render_threads")
|
|
|
|
row = advcol.row(align=True)
|
|
row.prop(self, "use_bg_col", toggle=True)
|
|
if self.use_bg_col:
|
|
row.prop(self, "bg_color", text="")
|
|
|
|
|
|
|
|
# Node to configure pass settings, which can be pinned as global
|
|
class BakeWrangler_SampleSettings(BakeWrangler_Tree_Node, Node):
|
|
'''Pass settings node'''
|
|
bl_label = 'Sample Settings'
|
|
bl_width_default = 144
|
|
|
|
# Inputs are static (none)
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
# Only one of this node can be pinned at a time
|
|
def pin_node(self, context):
|
|
if self.pinned:
|
|
tree = self.id_data
|
|
for node in tree.nodes:
|
|
if node != self and node.bl_idname == 'BakeWrangler_SampleSettings':
|
|
node.pinned = False
|
|
|
|
# Props
|
|
pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
|
|
bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 1 for all PBR passes and Normal maps. Values over 50 generally won't improve results.\nQuality is gained by increasing resolution rather than samples past that point", default=1, min=1)
|
|
bake_threshold: bpy.props.FloatProperty(name="Noise Threshold", description="Noise level to stop sampling at if reached before sample count", default=0.01, min=0.001, max=1.0)
|
|
bake_usethresh: bpy.props.BoolProperty(name="Use Threshold", description="Enables use of noise level threshold", default=False)
|
|
bake_timelimit: bpy.props.FloatProperty(name="Time Limit", description="Maximum time to spend on a single bake. Zero to disable", default=0.0, min=0.0, subtype='TIME_ABSOLUTE', unit='TIME_ABSOLUTE', step=100)
|
|
|
|
def copy(self, node):
|
|
self.pinned = False
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN (none)
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_SampleSetting', "Sample Settings")
|
|
# Prefs
|
|
self.bake_samples = _prefs("def_samples")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
colnode = layout.column(align=False)
|
|
|
|
colbasic = colnode.column(align=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.label(text="Noise")
|
|
rowbasic.prop(self, "bake_usethresh", text="")
|
|
rowbasic.prop(self, "bake_threshold", text="")
|
|
colbasic.prop(self, "bake_samples", text="Samples")
|
|
colbasic.prop(self, "bake_timelimit")
|
|
|
|
|
|
# Node to configure output settings, which can be pinned as global
|
|
class BakeWrangler_OutputSettings(BakeWrangler_Tree_Node, Node):
|
|
'''Output settings node'''
|
|
bl_label = 'Output Settings'
|
|
bl_width_default = 152
|
|
|
|
# Inputs are static (none)
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
# Only one of this node can be pinned at a time
|
|
def pin_node(self, context):
|
|
if self.pinned:
|
|
tree = self.id_data
|
|
for node in tree.nodes:
|
|
if node != self and node.bl_idname == 'BakeWrangler_OutputSettings':
|
|
node.pinned = False
|
|
|
|
# Update output nodes to display alpha input or not depending on setting
|
|
def check_alpha(self, context):
|
|
tree = self.id_data
|
|
for node in tree.nodes:
|
|
if node.bl_idname == 'BakeWrangler_Output_Image_Path':
|
|
node.update_inputs()
|
|
|
|
# Recreate image format drop down as the built in one doesn't seem usable? Also most of the settings
|
|
# for the built in image settings selector don't seem applicable to saving from script...
|
|
img_format = (
|
|
('BMP', "BMP", "Output image in bitmap format."),
|
|
('IRIS', "Iris", "Output image in (old!) SGI IRIS format."),
|
|
('PNG', "PNG", "Output image in PNG format."),
|
|
('JPEG', "JPEG", "Output image in JPEG format."),
|
|
('JPEG2000', "JPEG 2000", "Output image in JPEG 2000 format."),
|
|
('TARGA', "Targa", "Output image in Targa format."),
|
|
('TARGA_RAW', "Targa Raw", "Output image in uncompressed Targa format."),
|
|
('CINEON', "Cineon", "Output image in Cineon format."),
|
|
('DPX', "DPX", "Output image in DPX format."),
|
|
('OPEN_EXR_MULTILAYER', "OpenEXR MultiLayer", "Output image in multilayer OpenEXR format."),
|
|
('OPEN_EXR', "OpenEXR", "Output image in OpenEXR format."),
|
|
('HDR', "Radiance HDR", "Output image in Radiance HDR format."),
|
|
('TIFF', "TIFF", "Output image in TIFF format."),
|
|
)
|
|
|
|
img_color_modes = (
|
|
('BW', "BW", "Image saved in 8 bit grayscale"),
|
|
('RGB', "RGB", "Image saved with RGB (color) data"),
|
|
('RGBA', "RGBA", "Image saved with RGB and Alpha data"),
|
|
)
|
|
|
|
img_color_modes_noalpha = (
|
|
('BW', "BW", "Image saved in 8 bit grayscale"),
|
|
('RGB', "RGB", "Image saved with RGB (color) data"),
|
|
)
|
|
|
|
img_color_depths_8_16 = (
|
|
('8', "8", "8 bit color channels"),
|
|
('16', "16", "16 bit color channels"),
|
|
)
|
|
|
|
img_color_depths_8_12_16 = (
|
|
('8', "8", "8 bit color channels"),
|
|
('12', "12", "12 bit color channels"),
|
|
('16', "16", "16 bit color channels"),
|
|
)
|
|
|
|
img_color_depths_8_10_12_16 = (
|
|
('8', "8", "8 bit color channels"),
|
|
('10', "10", "10 bit color channels"),
|
|
('12', "12", "12 bit color channels"),
|
|
('16', "16", "16 bit color channels"),
|
|
)
|
|
|
|
img_color_depths_16_32 = (
|
|
('16', "Float (Half)", "16 bit color channels"),
|
|
('32', "Float (Full)", "32 bit color channels"),
|
|
)
|
|
|
|
img_codecs_jpeg2k = (
|
|
('JP2', "JP2", ""),
|
|
('J2K', "J2K", ""),
|
|
)
|
|
|
|
img_codecs_openexr = (
|
|
('DWAA', "DWAA (lossy)", ""),
|
|
('B44A', "B44A (lossy)", ""),
|
|
('ZIPS', "ZIPS (lossless)", ""),
|
|
('RLE', "RLE (lossless)", ""),
|
|
('RLE', "RLE (lossless)", ""),
|
|
('PIZ', "PIZ (lossless)", ""),
|
|
('ZIP', "ZIP (lossless)", ""),
|
|
('PXR24', "Pxr24 (lossy)", ""),
|
|
('NONE', "None", ""),
|
|
)
|
|
|
|
img_codecs_tiff = (
|
|
('PACKBITS', "Pack Bits", ""),
|
|
('LZW', "LZW", ""),
|
|
('DEFLATE', "Deflate", ""),
|
|
('NONE', "None", ""),
|
|
)
|
|
|
|
img_color_spaces = []
|
|
for space in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys():
|
|
img_color_spaces.append((space, space, space))
|
|
img_color_spaces = tuple(img_color_spaces)
|
|
|
|
# Props
|
|
pinned: bpy.props.BoolProperty(name="Pin", description="When pinned, use settings globally in node tree", default=False, update=pin_node)
|
|
img_xres: bpy.props.IntProperty(name="Image X resolution", description="Number of horizontal pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
|
|
img_yres: bpy.props.IntProperty(name="Image Y resolution", description="Number of vertical pixels in image. Bake pass data will be scaled to fit the image size. Power of 2 sizes are usually best for exporting", default=2048, min=1, subtype='PIXEL')
|
|
img_clear: bpy.props.BoolProperty(name="Clear Image", description="Clear image before writing bake data", default=False)
|
|
img_udim: bpy.props.BoolProperty(name="UDIM", description="Treat UV map as UDIM space and append standard number system to file name", default=False)
|
|
img_type: bpy.props.EnumProperty(name="Image Format", description="File format to save bake as", items=img_format, default='PNG')
|
|
fast_aa: bpy.props.BoolProperty(name="Fast Anti-Alias", description="Fast Anti-Aliasing. For more control use down or up sampling of bake to output by using different resolutions", default=False)
|
|
fast_aa_lvl: bpy.props.IntProperty(name="Fast AA Level", description="Level of fast AA to apply from 1 to 9", default=3, min=1, max=9)
|
|
|
|
marginer: bpy.props.BoolProperty(name="Marginer", description="Use alternative margin generator (slower)", default=False)
|
|
marginer_size: bpy.props.IntProperty(name="Marginer Size", description="Extends the baked result as a post process filter", default=0, min=0, subtype='PIXEL')
|
|
marginer_fill: bpy.props.BoolProperty(name="Marginer Fill", description="Fill all gaps with margin instead of using a fixed width", default=False)
|
|
|
|
adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Display or hide advanced settings", default=False)
|
|
|
|
# Core settings
|
|
img_color_space: bpy.props.EnumProperty(name="Color Space", description="Color space to use when saving the image", items=img_color_spaces)
|
|
img_use_float: bpy.props.BoolProperty(name="Use 32 Bit Float", description="Generate all input passes using 32 bit floating point color (128 bits per pixel). Note this isn't very useful if your image format isn't set to a high bit depth", default=False)
|
|
img_color_mode: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels, and RGBA for saving red, green, blue and alpha channels", items=img_color_modes, default='RGB', update=check_alpha)
|
|
img_color_mode_noalpha: bpy.props.EnumProperty(name="Color", description="Choose BW for saving grayscale images, RGB for saving red, green and blue channels", items=img_color_modes_noalpha, default='RGB')
|
|
img_non_color: bpy.props.StringProperty(name="Non Color Space", default="NONE")
|
|
|
|
# Color Depths
|
|
img_color_depth_8_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_16, default='8')
|
|
img_color_depth_8_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_12_16, default='8')
|
|
img_color_depth_8_10_12_16: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_8_10_12_16, default='8')
|
|
img_color_depth_16_32: bpy.props.EnumProperty(name="Color Depth", description="Bit depth per channel", items=img_color_depths_16_32, default='16')
|
|
|
|
# Compression / Quality
|
|
img_compression: bpy.props.IntProperty(name="Compression", description="Amount of time to determine best compression: 0 = no compression, 100 = maximum lossless compression", default=15, min=0, max=100, subtype='PERCENTAGE')
|
|
img_quality: bpy.props.IntProperty(name="Quality", description="Quality for image formats that support lossy compression", default=90, min=0, max=100, subtype='PERCENTAGE')
|
|
|
|
# Codecs
|
|
img_codec_jpeg2k: bpy.props.EnumProperty(name="Codec", description="Codec settings for jpeg2000", items=img_codecs_jpeg2k, default='JP2')
|
|
img_codec_openexr: bpy.props.EnumProperty(name="Codec", description="Codec settings for OpenEXR", items=img_codecs_openexr, default='ZIP')
|
|
img_codec_tiff: bpy.props.EnumProperty(name="Compression", description="Compression mode for TIFF", items=img_codecs_tiff, default='DEFLATE')
|
|
|
|
# Other random image format settings
|
|
img_jpeg2k_cinema: bpy.props.BoolProperty(name="Cinema", description="Use Openjpeg Cinema Preset", default=True)
|
|
img_jpeg2k_cinema48: bpy.props.BoolProperty(name="Cinema (48)", description="Use Openjpeg Cinema Preset (48 fps)", default=False)
|
|
img_jpeg2k_ycc: bpy.props.BoolProperty(name="YCC", description="Save luminance-chrominance-chrominance channels instead of RGB colors", default=False)
|
|
img_dpx_log: bpy.props.BoolProperty(name="Log", description="Convert to logarithmic color space", default=False)
|
|
img_openexr_zbuff: bpy.props.BoolProperty(name="Z Buffer", description="Save the z-depth per pixel (32 bit unsigned int z-buffer)", default=True)
|
|
|
|
def copy(self, node):
|
|
self.pinned = False
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN (none)
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_OutputSetting', "Output Settings")
|
|
# Prefs
|
|
self.img_type = self.img_format[_prefs("def_format")][0]
|
|
self.img_xres = _prefs("def_xout")
|
|
self.img_yres = _prefs("def_yout")
|
|
self.adv_settings = _prefs("def_show_adv")
|
|
self.img_color_space = bpy.data.scenes[0].sequencer_colorspace_settings.name
|
|
|
|
def draw_buttons(self, context, layout):
|
|
colnode = layout.column(align=False)
|
|
|
|
colbasic = colnode.column(align=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "img_xres", text="X")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_xres")
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "img_yres", text="Y")
|
|
BakeWrangler_Tree_Node.draw_double_halve(self, rowbasic, "img_yres")
|
|
rowbasic = colbasic.row(align=True)
|
|
rowbasic.prop(self, "img_clear", text="Clear", toggle=True)
|
|
rowbasic.prop(self, "img_udim", toggle=True)
|
|
rowbasic = colbasic.row(align=True)
|
|
if not self.fast_aa:
|
|
rowbasic.prop(self, "fast_aa", toggle=True)
|
|
if self.fast_aa:
|
|
rowbasic.prop(self, "fast_aa", toggle=True, text="Fast AA:")
|
|
rowbasic.prop(self, "fast_aa_lvl", text="")
|
|
|
|
split = colnode.split(factor=0.35)
|
|
split.label(text="Format:")
|
|
split.prop(self, "img_type", text="")
|
|
|
|
advrow = colnode.row()
|
|
advrow.alignment = 'LEFT'
|
|
if not self.adv_settings:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
else:
|
|
advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
coladv = colnode.column(align=True)
|
|
|
|
row = coladv.row(align=True)
|
|
if self.marginer:
|
|
row.prop(self, "marginer", toggle=True, icon_only=True, icon='NODE_INSERT_OFF')
|
|
else:
|
|
row.prop(self, "marginer", toggle=True, icon='NODE_INSERT_OFF')
|
|
if self.marginer:
|
|
row_size = row.row(align=True)
|
|
row_size.prop(self, "marginer_size", text="")
|
|
if self.marginer_fill:
|
|
row_size.enabled = False
|
|
row.prop(self, "marginer_fill", toggle=True, icon_only=True, icon='TPAINT_HLT')
|
|
|
|
coladv.prop(self, "img_use_float", toggle=True)
|
|
|
|
splitadv = coladv.split(factor=0.4)
|
|
coladvtxt = splitadv.column(align=True)
|
|
coladvopt = splitadv.column(align=True)
|
|
|
|
# Color Spaces
|
|
if self.img_type != 'CINEON':
|
|
coladvtxt.label(text="Space:")
|
|
coladvopt.prop(self, "img_color_space", text="")
|
|
# Color Modes
|
|
if self.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']:
|
|
coladvtxt.label(text="Color:")
|
|
coladvopt.prop(self, "img_color_mode_noalpha", text="")
|
|
if self.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']:
|
|
coladvtxt.label(text="Color:")
|
|
coladvopt.prop(self, "img_color_mode", text="")
|
|
# Color Depths
|
|
if self.img_type in ['PNG', 'TIFF']:
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_8_16", text="")
|
|
if self.img_type == 'JPEG2000':
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_8_12_16", text="")
|
|
if self.img_type == 'DPX':
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_8_10_12_16", text="")
|
|
if self.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']:
|
|
coladvtxt.label(text="Depth:")
|
|
coladvopt.prop(self, "img_color_depth_16_32", text="")
|
|
# Compression / Quality
|
|
if self.img_type == 'PNG':
|
|
coladvtxt.label(text="Compression:")
|
|
coladvopt.prop(self, "img_compression", text="")
|
|
if self.img_type in ['JPEG', 'JPEG2000']:
|
|
coladvtxt.label(text="Quality:")
|
|
coladvopt.prop(self, "img_quality", text="")
|
|
# Codecs
|
|
if self.img_type == 'JPEG2000':
|
|
coladvtxt.label(text="Codec:")
|
|
coladvopt.prop(self, "img_codec_jpeg2k", text="")
|
|
if self.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']:
|
|
coladvtxt.label(text="Codec:")
|
|
coladvopt.prop(self, "img_codec_openexr", text="")
|
|
if self.img_type == 'TIFF':
|
|
coladvtxt.label(text="Compression:")
|
|
coladvopt.prop(self, "img_codec_tiff", text="")
|
|
# Other random image settings
|
|
if self.img_type == 'JPEG2000':
|
|
coladv.prop(self, "img_jpeg2k_cinema")
|
|
coladv.prop(self, "img_jpeg2k_cinema48")
|
|
coladv.prop(self, "img_jpeg2k_ycc")
|
|
if self.img_type == 'DPX':
|
|
coladv.prop(self, "img_dpx_log")
|
|
if self.img_type == 'OPEN_EXR':
|
|
coladv.prop(self, "img_openexr_zbuff")
|
|
|
|
|
|
|
|
# File names input to allow attaching prefixes to outputs and make object->filename system more intuitive
|
|
class BakeWrangler_Input_Filenames(BakeWrangler_Tree_Node, Node):
|
|
'''File Names node'''
|
|
bl_label = 'File Names'
|
|
bl_width_default = 198
|
|
|
|
def get_names(self):
|
|
names = []
|
|
for input in self.inputs:
|
|
if input.bl_idname == 'BakeWrangler_Socket_ObjectNames' and get_input(input):
|
|
names.append(get_input(input))
|
|
return names
|
|
|
|
def get_frames(self, padding=False, animated=False):
|
|
if animated: return self.use_rnd_seed
|
|
frames = []
|
|
pad = None
|
|
# Parse string
|
|
ranges = self.frame_ranges.split(sep=",")
|
|
import re
|
|
extract = re.compile(r'(\D*#([0-9]*)\D*)|(\D*([0-9]*)-?([0-9]*):?([0-9]*)\D*)')
|
|
for arange in ranges:
|
|
match = extract.match(arange)
|
|
if match.group(1) and padding:
|
|
pad = int(match.group(2))
|
|
elif match.group(3) and not padding:
|
|
start = int(match.group(4))
|
|
end = int(match.group(5)) if match.group(5) else None
|
|
step = int(match.group(6)) if match.group(6) else 1
|
|
if end:
|
|
if end < start:
|
|
step *= -1
|
|
end -= 1
|
|
else: end +=1
|
|
for f in range(start, end, step): frames.append(f)
|
|
else:
|
|
frames.append(start)
|
|
else: continue
|
|
if padding:
|
|
return pad
|
|
return set(frames)
|
|
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_ObjectNames', "Object Names")
|
|
|
|
# Props
|
|
frame_ranges: bpy.props.StringProperty(name="Frame Ranges", description="Comma separated list of frame ranges to bake (eg: 1,3,4-12). For non-default zero padding include #number_of_zeros as one of your ranges (eg: #3,1-3,10 to pad all numbers to 3). Frame numbers are added to the end of file names", default="")
|
|
use_rnd_seed: bpy.props.BoolProperty(name="Use Animated Seed", description="Use different seed values (and hence noise patterns) at different frames", default=True)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_SplitOutput', "Path/Filename")
|
|
self.inputs.new('BakeWrangler_Socket_ObjectNames', "Object Names")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_SplitOutput', "Path / Filenames / Frames")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
col = layout.column(align=True)
|
|
row0 = col.row()
|
|
row0.label(text="Frame Ranges:")
|
|
row1 = col.row(align=True)
|
|
row1.prop(self, "frame_ranges", text="")
|
|
row1.prop(self, "use_rnd_seed", text="", icon='TIME')
|
|
|
|
|
|
|
|
# Input node that contains a list of objects relevant to baking
|
|
class BakeWrangler_Input_ObjectList(BakeWrangler_Tree_Node, Node):
|
|
'''Object list node'''
|
|
bl_label = 'Objects'
|
|
bl_width_default = 198
|
|
|
|
# Makes sure there is always one empty input socket at the bottom by adding and removing sockets
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Object', "Object")
|
|
|
|
# Determine if object meets current input filter
|
|
def input_filter(self, input_name, object):
|
|
if self.filter_collection:
|
|
if object.rna_type.identifier == 'Collection':
|
|
return True
|
|
elif object.rna_type.identifier == 'Object':
|
|
if (self.filter_mesh and object.type == 'MESH') or \
|
|
(self.filter_curve and object.type == 'CURVE') or \
|
|
(self.filter_surface and object.type == 'SURFACE') or \
|
|
(self.filter_meta and object.type == 'META') or \
|
|
(self.filter_font and object.type == 'FONT') or \
|
|
(self.filter_light and object.type == 'LIGHT'):
|
|
return True
|
|
return False
|
|
|
|
# Get all objects in tree from this node (mostly just uses the sockets methods)
|
|
def get_objects(self, only_mesh=False, no_lights=False, only_groups=False):
|
|
objects = []
|
|
for input in self.inputs:
|
|
in_objs = input.get_objects(only_mesh, no_lights, only_groups)
|
|
if len(in_objs):
|
|
objects += in_objs
|
|
return objects
|
|
|
|
# Validate all objects in tree from this node (mostly just uses the sockets methods)
|
|
def validate(self, check_materials=False, check_as_active=False, check_multi=False):
|
|
valid = [True]
|
|
for input in self.inputs:
|
|
valid_input = input.validate(check_materials, check_as_active, check_multi)
|
|
if not valid_input.pop(0):
|
|
valid[0] = False
|
|
if len(valid_input):
|
|
valid += valid_input
|
|
return valid
|
|
|
|
filter_mesh: bpy.props.BoolProperty(name="Meshes", description="Show mesh type objects", default=True)
|
|
filter_curve: bpy.props.BoolProperty(name="Curves", description="Show curve type objects", default=True)
|
|
filter_surface: bpy.props.BoolProperty(name="Surfaces", description="Show surface type objects", default=True)
|
|
filter_meta: bpy.props.BoolProperty(name="Metas", description="Show meta type objects", default=True)
|
|
filter_font: bpy.props.BoolProperty(name="Fonts", description="Show font type objects", default=True)
|
|
filter_light: bpy.props.BoolProperty(name="Lights", description="Show light type objects", default=True)
|
|
filter_collection: bpy.props.BoolProperty(name="Collections", description="Toggle only collections", default=False)
|
|
|
|
def copy(self, node):
|
|
self.inputs.clear()
|
|
for sok in node.inputs:
|
|
csok = self.inputs.new('BakeWrangler_Socket_Object', "Object")
|
|
csok.value = sok.value
|
|
csok.type = sok.type
|
|
csok.recursive = sok.recursive
|
|
csok.pick_uv = sok.pick_uv
|
|
csok.uv_map = sok.uv_map
|
|
csok.use_cage = sok.use_cage
|
|
csok.cage = sok.cage
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Object', "Object")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Object', "Objects")
|
|
# Prefs
|
|
self.filter_mesh = _prefs("def_filter_mesh")
|
|
self.filter_curve = _prefs("def_filter_curve")
|
|
self.filter_surface = _prefs("def_filter_surface")
|
|
self.filter_meta = _prefs("def_filter_meta")
|
|
self.filter_font = _prefs("def_filter_font")
|
|
self.filter_light = _prefs("def_filter_light")
|
|
self.filter_collection = _prefs("def_filter_collection")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
row = layout.row(align=True)
|
|
row0 = row.row()
|
|
row0.label(text="Filter:")
|
|
|
|
row1 = row.row(align=True)
|
|
row1.alignment = 'RIGHT'
|
|
for fltr in BakeWrangler_Operator_FilterToggle.filters:
|
|
icn = fltr.split("_")[1].upper() + "_DATA"
|
|
op = row1.operator("bake_wrangler.filter_toggle", icon=icn, text="", depress=getattr(self, fltr))
|
|
op.tree = self.id_data.name
|
|
op.node = self.name
|
|
op.filter = fltr
|
|
if self.filter_collection:
|
|
row1.enabled = False
|
|
|
|
row2 = row.row(align=False)
|
|
row2.alignment = 'RIGHT'
|
|
row2.prop(self, "filter_collection", text="", icon='GROUP')
|
|
|
|
|
|
|
|
# Automatic sorting of meshes into groups for baking
|
|
class BakeWrangler_Sort_Meshes(BakeWrangler_Tree_Node, Node):
|
|
'''Sorting and grouping of meshes by name'''
|
|
bl_label = 'Auto Sort Meshes'
|
|
bl_width_default = 240
|
|
|
|
# Inputs are static on this node
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Mesh', "Mesh")
|
|
|
|
# Try to get nodes settings from local or global node
|
|
def get_settings(self, validating=False, input=None):
|
|
if input:
|
|
settings = get_input(input)
|
|
if settings:
|
|
return settings.get_settings()
|
|
return {}
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message.
|
|
def validate(self, check_materials=False, multires=False):
|
|
valid = [True]
|
|
has_valid_input = False
|
|
for inpt in self.inputs:
|
|
if inpt.islinked() and inpt.valid:
|
|
mesh = get_input(inpt)
|
|
if mesh:
|
|
input_valid = mesh.validate(check_materials, multires)
|
|
if not input_valid.pop(0):
|
|
valid[0] = False
|
|
else:
|
|
has_valid_input = True
|
|
if len(input_valid):
|
|
valid += input_valid
|
|
if not has_valid_input and len(valid) < 2:
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
|
|
return valid
|
|
|
|
# Return name with ident removed or empty string if ident not in name
|
|
def find_ident(self, type, name, ident, case_sens=True):
|
|
if type == 'PASS' or not ident:
|
|
return name
|
|
ident_idx = 0
|
|
ident_len = len(ident)
|
|
cname = name
|
|
cident = ident
|
|
if not case_sens:
|
|
cname = name.lower()
|
|
cident = ident.lower()
|
|
if type == 'STARTS':
|
|
if not cname.startswith(cident):
|
|
return ''
|
|
else: return name[ident_len:]
|
|
elif type == 'ENDS':
|
|
if not cname.endswith(cident):
|
|
return ''
|
|
else: return name[:-ident_len]
|
|
elif type in ['SCONTAINS', 'ECONTAINS']:
|
|
if type == 'SCONTAINS':
|
|
ident_idx = cname.find(cident)
|
|
if type == 'ECONTAINS':
|
|
ident_idx = cname.rfind(cident)
|
|
if ident_idx == -1:
|
|
return ''
|
|
else: return name[:ident_idx] + name[ident_idx + ident_len:]
|
|
|
|
# Return a list of object pairings
|
|
def get_objects(self, set, input):
|
|
objs = []
|
|
mesh = get_input(input)
|
|
if not mesh:
|
|
return []
|
|
if set == 'TARGET' and 'Target' in mesh.inputs.keys():
|
|
targets = prune_objects(mesh.inputs["Target"].get_objects(only_mesh=True), True)
|
|
sources = prune_objects(mesh.inputs["Source"].get_objects(no_lights=True))
|
|
sourgrp = prune_objects(mesh.inputs["Source"].get_objects(no_lights=True, only_groups=True))
|
|
scenes = prune_objects(mesh.inputs["Scene"].get_objects())
|
|
scengrp = prune_objects(mesh.inputs["Scene"].get_objects(only_groups=True))
|
|
if not len(sources) and self.high_search != 'PASS':
|
|
sources = prune_objects(mesh.inputs["Target"].get_objects(no_lights=True))
|
|
sourgrp = prune_objects(mesh.inputs["Target"].get_objects(no_lights=True, only_groups=True))
|
|
|
|
# Create pairings of high to low, first ident possible low polys
|
|
for obj in targets:
|
|
low_name = self.find_ident(self.low_search, obj[0].name, self.low_string, self.low_case)
|
|
high_obj = []
|
|
scen_obj = []
|
|
if not low_name: continue
|
|
# Create the high poly name string based on the possible low objects name
|
|
if self.high_search == 'PASS':
|
|
high_obj = sources
|
|
else:
|
|
if self.high_collect:
|
|
for high in sourgrp:
|
|
high_name = self.find_ident(self.high_search, high[0].name, self.high_string, self.high_case)
|
|
if high_name == low_name:
|
|
high_obj += high[1]
|
|
if not len(high_obj):
|
|
for high in sources:
|
|
high_name = self.find_ident(self.high_search, high[0].name, self.high_string, self.high_case)
|
|
if high_name == low_name:
|
|
high_obj.append(high)
|
|
# Check scene objects (if no scene string is set, scene is included as is)
|
|
if self.scene_search == 'PASS':
|
|
scen_obj = scenes
|
|
else:
|
|
if self.scene_collect:
|
|
for scen in scengrp:
|
|
scen_name = self.find_ident(self.scene_search, scen[0].name, self.scene_string, self.scene_case)
|
|
if scen_name == low_name:
|
|
scen_obj += scen[1]
|
|
if not len(scen_obj):
|
|
for scen in scenes:
|
|
scen_name = self.find_ident(self.scene_search, scen[0].name, self.scene_string, self.scene_case)
|
|
if scen_name == low_name:
|
|
scen_obj.append(scen)
|
|
# Only paired objects will be added
|
|
if (len(high_obj) and self.high_search != 'PASS') or (len(scen_obj) and self.scene_search != 'PASS') or self.low_search == 'PASS':
|
|
objs.append([obj, high_obj, scen_obj, low_name])
|
|
else: pass
|
|
|
|
# Return pruned object list
|
|
return objs
|
|
|
|
# Get a list of unique objects used as either source or target
|
|
def get_unique_objects(self, type, for_auto_cage=False, input=None):
|
|
if type not in ['TARGET', 'SOURCE']:
|
|
return []
|
|
objs_set = []
|
|
objs = self.get_objects('TARGET', input)
|
|
if len(objs):
|
|
if for_auto_cage:
|
|
settings = self.get_settings(input=input)
|
|
if not settings['auto_cage']:
|
|
return []
|
|
for obj in objs:
|
|
if type == 'TARGET':
|
|
if for_auto_cage:
|
|
if len(obj[0]) > 2 and obj[0][2]:
|
|
continue
|
|
objs_set.append([obj[0][0], settings['acage_expansion'], settings['acage_smooth']])
|
|
else:
|
|
objs_set.append(obj[0])
|
|
elif type == 'SOURCE':
|
|
objs_set.append(obj[1])
|
|
else:
|
|
return []
|
|
return objs_set
|
|
|
|
search_type = (
|
|
('PASS', "Pass Through", "Pass all items through without doing any matching", 'ANIM', 0),
|
|
('STARTS', "Starts", "Starts with ID", 'TRIA_RIGHT', 1),
|
|
('ENDS', "Ends", "Ends with ID", 'TRIA_LEFT', 2),
|
|
('SCONTAINS', "Contains ->", "Contains ID, searching from start", 'NEXT_KEYFRAME', 3),
|
|
('ECONTAINS', "Contains <-", "Contains ID, searching from end", 'PREV_KEYFRAME', 4),
|
|
)
|
|
|
|
low_search: bpy.props.EnumProperty(name="Target Search", description="How to search for target identifier", items=search_type, default='PASS')
|
|
low_string: bpy.props.StringProperty(name="Target Ident", description="Target identifier")
|
|
low_case: bpy.props.BoolProperty(name="Target case sensitivity", description="Use case sensitive matching", default=True)
|
|
high_string: bpy.props.StringProperty(name="Source Ident", description="Source identifier")
|
|
high_search: bpy.props.EnumProperty(name="Source Search", description="How to search for source identifier", items=search_type, default='PASS')
|
|
high_collect: bpy.props.BoolProperty(name="Match Collections", description="Try to match a source collection before trying the items inside it", default=True)
|
|
high_case: bpy.props.BoolProperty(name="Source case sensitivity", description="Use case sensitive matching", default=True)
|
|
scene_string: bpy.props.StringProperty(name="Scene Ident", description="Scene identifier")
|
|
scene_search: bpy.props.EnumProperty(name="Scene Search", description="How to search for scene identifier", items=search_type, default='PASS')
|
|
scene_collect: bpy.props.BoolProperty(name="Match Collections", description="Try to match a scene collection before trying the items inside it", default=True)
|
|
scene_case: bpy.props.BoolProperty(name="Scene case sensitivity", description="Use case sensitive matching", default=True)
|
|
|
|
show_groupings: bpy.props.BoolProperty(name="Show Groupings", default=False)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Mesh', "Mesh")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
colnode = layout.column(align=True)
|
|
split_fac = 60 / self.width
|
|
split = colnode.split(factor=split_fac)
|
|
split.label(text="Target ID:")
|
|
row = split.row(align=True)
|
|
row1 = row.row(align=True)
|
|
row2 = row.row(align=True)
|
|
row1.prop(self, "low_string", text="")
|
|
row1.prop(self, "low_case", text="", icon_only=True, icon='SMALL_CAPS')
|
|
row2.prop(self, "low_search", text="", icon_only=True)
|
|
if self.low_search == 'PASS':
|
|
row1.enabled = False
|
|
|
|
split = colnode.split(factor=split_fac)
|
|
split.label(text="Source ID:")
|
|
row = split.row(align=True)
|
|
row1 = row.row(align=True)
|
|
row2 = row.row(align=True)
|
|
row1.prop(self, "high_string", text="")
|
|
row1.prop(self, "high_collect", text="", icon_only=True, icon='OUTLINER_COLLECTION')
|
|
row1.prop(self, "high_case", text="", icon_only=True, icon='SMALL_CAPS')
|
|
row2.prop(self, "high_search", text="", icon_only=True)
|
|
if self.high_search == 'PASS':
|
|
row1.enabled = False
|
|
|
|
split = colnode.split(factor=split_fac)
|
|
split.label(text="Scene ID:")
|
|
row = split.row(align=True)
|
|
row1 = row.row(align=True)
|
|
row2 = row.row(align=True)
|
|
row1.prop(self, "scene_string", text="")
|
|
row1.prop(self, "scene_collect", text="", icon_only=True, icon='OUTLINER_COLLECTION')
|
|
row1.prop(self, "scene_case", text="", icon_only=True, icon='SMALL_CAPS')
|
|
row2.prop(self, "scene_search", text="", icon_only=True)
|
|
if self.scene_search == 'PASS':
|
|
row1.enabled = False
|
|
|
|
def draw_buttons_ext(self, context, layout):
|
|
layout.prop(self, 'show_groupings')
|
|
if self.show_groupings:
|
|
for input in self.inputs:
|
|
mesh = get_input(input)
|
|
if mesh:
|
|
layout.label(text=mesh.get_name())
|
|
box = layout.box()
|
|
for obj_grp in self.get_objects('TARGET', input):
|
|
boxin = box.box()
|
|
col = boxin.column(align=True)
|
|
row = col.row()
|
|
row.label(text=obj_grp[0][0].name)
|
|
row.label(text="", icon=obj_grp[0][0].type + '_DATA')
|
|
col.label(text="-Source:")
|
|
for hi in obj_grp[1]:
|
|
row = col.row()
|
|
row.label(text=" " + hi[0].name)
|
|
row.label(text="", icon=hi[0].type + '_DATA')
|
|
col.label(text="-Scene:")
|
|
for scen in obj_grp[2]:
|
|
row = col.row()
|
|
row.label(text=" " + scen[0].name)
|
|
row.label(text="", icon=scen[0].type + '_DATA')
|
|
|
|
|
|
|
|
# Settings to be used when baking a billboard
|
|
class BakeWrangler_Bake_Billboard(BakeWrangler_Tree_Node, Node):
|
|
'''Mesh input node'''
|
|
bl_label = 'Input Billboard'
|
|
bl_width_default = 240
|
|
|
|
# Inputs are static on this node
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
# Determine if object meets current input filter
|
|
def input_filter(self, input_name, object):
|
|
if input_name == "Target":
|
|
if object.type == 'MESH':
|
|
return True
|
|
elif input_name == "Source":
|
|
if object.type in ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT']:
|
|
return True
|
|
return False
|
|
|
|
# Try to get nodes settings from local or global node
|
|
def get_settings(self, validating=False):
|
|
settings = get_input(self.inputs["Settings"])
|
|
if not settings:
|
|
settings = self.id_data.get_pinned_settings("MeshSettings")
|
|
if validating:
|
|
return settings
|
|
mesh_settings = {}
|
|
mesh_settings['ray_dist'] = 0
|
|
mesh_settings['max_ray_dist'] = 0
|
|
mesh_settings['margin'] = settings.margin
|
|
mesh_settings['margin_extend'] = settings.margin_extend
|
|
mesh_settings['margin_auto'] = settings.margin_auto
|
|
#mesh_settings['marginer'] = settings.marginer
|
|
#mesh_settings['marginer_fill'] = settings.marginer_fill
|
|
#mesh_settings['mask_margin'] = settings.mask_margin
|
|
mesh_settings['auto_cage'] = False
|
|
mesh_settings['acage_expansion'] = 0
|
|
mesh_settings['acage_smooth'] = 0
|
|
mesh_settings['material_replace'] = False
|
|
mesh_settings['material_override'] = None
|
|
mesh_settings['material_osl'] = False
|
|
mesh_settings['bake_mods'] = False
|
|
mesh_settings['bake_mods_invert'] = False
|
|
mesh_settings['alpha_bounce'] = self.alpha_bounce
|
|
return mesh_settings
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message.
|
|
def validate(self, check_materials=False, multires=False):
|
|
valid = [True]
|
|
# Check settings are set somewhere
|
|
if not self.get_settings(validating=True):
|
|
valid[0] = False
|
|
valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected mesh settings found"])
|
|
# Check source objects
|
|
has_selected = False
|
|
if not multires:
|
|
has_selected = len(self.inputs["Source"].get_objects()) > 0
|
|
if has_selected and check_materials:
|
|
valid_selected = self.inputs["Source"].validate(check_materials)
|
|
# Add any generated messages to the stack. Material errors wont stop bake
|
|
if len(valid_selected) > 1:
|
|
valid_selected.pop(0)
|
|
valid += valid_selected
|
|
# Check target meshes
|
|
has_active = len(self.inputs["Target"].get_objects(True)) > 0
|
|
if has_active:
|
|
valid_active = self.inputs["Target"].validate(check_materials and not has_selected, True, multires)
|
|
valid[0] = valid_active.pop(0)
|
|
# Add any generated messages to the stack. Errors here will stop bake
|
|
if len(valid_active):
|
|
valid += valid_active
|
|
else:
|
|
valid[0] = False
|
|
valid.append([_print("Target error", node=self, ret=True), ": No valid target objects selected"])
|
|
return valid
|
|
|
|
# Return the requested set of objects from the appropriate input socket
|
|
def get_objects(self, set):
|
|
#if _prefs("debug"): _print("Getting objects in %s" % (set))
|
|
if set == 'TARGET':
|
|
objs = prune_objects(self.inputs["Target"].get_objects(only_mesh=True), True)
|
|
elif set == 'SOURCE':
|
|
objs = prune_objects(self.inputs["Source"].get_objects(no_lights=True))
|
|
elif set == 'SCENE':
|
|
objs = []
|
|
# Return pruned object list
|
|
return objs
|
|
|
|
# Get a list of unique objects used as either source or target
|
|
def get_unique_objects(self, type, for_auto_cage=False):
|
|
if for_auto_cage:
|
|
return []
|
|
objs = self.get_objects(type)
|
|
return objs
|
|
|
|
alpha_bounce: bpy.props.IntProperty(name="Alpha Bounces", description="Number of times a ray can pass through transparent surfaces", default=3, min=1)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_MeshSetting', "Settings")
|
|
self.inputs.new('BakeWrangler_Socket_Object', "Target")
|
|
self.inputs.new('BakeWrangler_Socket_Object', "Source")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
layout.prop(self, "alpha_bounce")
|
|
|
|
|
|
# Bake materials as a texture by projecting them on a plane
|
|
class BakeWrangler_Bake_Material(BakeWrangler_Tree_Node, Node):
|
|
'''Material input node'''
|
|
bl_label = 'Input Material'
|
|
bl_width_default = 240
|
|
|
|
# Makes sure there is always one empty input socket at the bottom by adding and removing sockets
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Material', "Material")
|
|
|
|
# Hard coded settings as the values are either not used or have only one meaningful possible value for this node
|
|
def get_settings(self, validating=False):
|
|
mesh_settings = {}
|
|
mesh_settings['ray_dist'] = 0
|
|
mesh_settings['max_ray_dist'] = 0
|
|
mesh_settings['margin'] = 0
|
|
mesh_settings['margin_extend'] = False
|
|
mesh_settings['margin_auto'] = False
|
|
#mesh_settings['marginer'] = False
|
|
#mesh_settings['mask_margin'] = 0
|
|
mesh_settings['auto_cage'] = False
|
|
mesh_settings['acage_expansion'] = 0
|
|
mesh_settings['acage_smooth'] = 0
|
|
mesh_settings['material_replace'] = False
|
|
mesh_settings['material_override'] = None
|
|
mesh_settings['material_osl'] = False
|
|
mesh_settings['bake_mods'] = False
|
|
mesh_settings['bake_mods_invert'] = False
|
|
mesh_settings['matbk_width'] = self.mat_width
|
|
mesh_settings['matbk_height'] = self.mat_height
|
|
return mesh_settings
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message.
|
|
def validate(self, check_materials=False, multires=False):
|
|
valid = [True]
|
|
mats = self.get_materials()
|
|
# Check is has some materials
|
|
if not len(mats):
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": No valid inputs"])
|
|
return valid
|
|
# Check the materials can be baked if required
|
|
if check_materials:
|
|
for mat in mats:
|
|
# Is node based?
|
|
if not mat[0].node_tree or not mat[0].node_tree.nodes:
|
|
valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> not a node based material" % (mat.name)])
|
|
continue
|
|
# Is a 'principled' material?
|
|
passed = False
|
|
for node in mat[0].node_tree.nodes:
|
|
if node.type == 'OUTPUT_MATERIAL' and node.target in ['CYCLES', 'ALL']:
|
|
if material_recursor(node):
|
|
passed = True
|
|
break
|
|
if not passed:
|
|
valid.append([_print("Material warning", node=self.node, ret=True), ": <%s> Output doesn't appear to be a valid combination of Principled and Mix shaders. Baked values will not be correct for this material." % (mat.name)])
|
|
return valid
|
|
|
|
# Create a list of unique materials from node inputs
|
|
def get_materials(self):
|
|
materials = []
|
|
# First collect any objects in the object input
|
|
objs = self.inputs[0].get_objects(no_lights=True)
|
|
# If there are any objects, collect their materials
|
|
for obj in objs:
|
|
for mat in obj[0].data.materials:
|
|
materials.append([mat])
|
|
# Next just add materials from the remaining inputs
|
|
for input in self.inputs:
|
|
if input.bl_idname == 'BakeWrangler_Socket_Material':
|
|
if input.value is not None:
|
|
materials.append([input.value])
|
|
# Now prune out any duplicates
|
|
return prune_objects(materials)
|
|
|
|
mat_width: bpy.props.FloatProperty(name="Width", description="Width of plane to project material on", precision=3, unit='LENGTH', default=1, min=0)
|
|
mat_height: bpy.props.FloatProperty(name="Height", description="Height of plane to project material on", precision=3, unit='LENGTH', default=1, min=0)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Object', "Materials from Objects")
|
|
self.inputs.new('BakeWrangler_Socket_Material', "Material")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Material', "Material")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
col = layout.column(align=True)
|
|
col.prop(self, "mat_width")
|
|
col.prop(self, "mat_height")
|
|
|
|
|
|
# Mesh settings to be used when baking attached objects
|
|
class BakeWrangler_Bake_Mesh(BakeWrangler_Tree_Node, Node):
|
|
'''Mesh input node'''
|
|
bl_label = 'Input Mesh'
|
|
bl_width_default = 240
|
|
|
|
# Inputs are static on this node
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
# Determine if object meets current input filter
|
|
def input_filter(self, input_name, object):
|
|
if input_name == "Target":
|
|
if object.type == 'MESH':
|
|
return True
|
|
elif input_name == "Source":
|
|
if object.type in ['MESH', 'CURVE', 'SURFACE', 'META', 'FONT']:
|
|
return True
|
|
elif input_name == "Scene":
|
|
if object.rna_type.identifier == 'Collection':
|
|
return True
|
|
return False
|
|
|
|
# Try to get nodes settings from local or global node
|
|
def get_settings(self, validating=False):
|
|
settings = get_input(self.inputs["Settings"])
|
|
if not settings:
|
|
settings = self.id_data.get_pinned_settings("MeshSettings")
|
|
if validating:
|
|
return settings
|
|
mesh_settings = {}
|
|
mesh_settings['ray_dist'] = settings.ray_dist
|
|
mesh_settings['max_ray_dist'] = settings.max_ray_dist
|
|
mesh_settings['margin'] = settings.margin
|
|
mesh_settings['margin_extend'] = settings.margin_extend
|
|
mesh_settings['margin_auto'] = settings.margin_auto
|
|
#mesh_settings['marginer'] = settings.marginer
|
|
#mesh_settings['marginer_fill'] = settings.marginer_fill
|
|
#mesh_settings['mask_margin'] = settings.mask_margin
|
|
mesh_settings['auto_cage'] = settings.auto_cage
|
|
mesh_settings['acage_expansion'] = settings.acage_expansion
|
|
mesh_settings['acage_smooth'] = settings.acage_smooth
|
|
mesh_settings['material_replace'] = settings.material_replace
|
|
mesh_settings['material_override'] = settings.material_override
|
|
mesh_settings['material_osl'] = settings.material_osl
|
|
mesh_settings['bake_mods'] = settings.bake_mods
|
|
mesh_settings['bake_mods_invert'] = _prefs("invert_bakemod")
|
|
if self.view_from == 'CAM' and self.view_cam:
|
|
mesh_settings['view_from'] = self.view_cam
|
|
return mesh_settings
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message.
|
|
def validate(self, check_materials=False, multires=False):
|
|
valid = [True]
|
|
# Check settings are set somewhere
|
|
if not self.get_settings(validating=True):
|
|
valid[0] = False
|
|
valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected mesh settings found"])
|
|
# Check source objects
|
|
has_selected = False
|
|
if not multires:
|
|
has_selected = len(self.inputs["Source"].get_objects()) > 0
|
|
if has_selected and check_materials:
|
|
valid_selected = self.inputs["Source"].validate(check_materials)
|
|
# Add any generated messages to the stack. Material errors wont stop bake
|
|
if len(valid_selected) > 1:
|
|
valid_selected.pop(0)
|
|
valid += valid_selected
|
|
# Check target meshes
|
|
has_active = len(self.inputs["Target"].get_objects(True)) > 0
|
|
if has_active:
|
|
valid_active = self.inputs["Target"].validate(check_materials and not has_selected, True, multires)
|
|
valid[0] = valid_active.pop(0)
|
|
# Add any generated messages to the stack. Errors here will stop bake
|
|
if len(valid_active):
|
|
valid += valid_active
|
|
else:
|
|
valid[0] = False
|
|
valid.append([_print("Target error", node=self, ret=True), ": No valid target objects selected"])
|
|
return valid
|
|
|
|
# Return the requested set of objects from the appropriate input socket
|
|
def get_objects(self, set):
|
|
#if _prefs("debug"): _print("Getting objects in %s" % (set))
|
|
if set == 'TARGET':
|
|
objs = prune_objects(self.inputs["Target"].get_objects(only_mesh=True), True)
|
|
elif set == 'SOURCE':
|
|
objs = prune_objects(self.inputs["Source"].get_objects(no_lights=True))
|
|
elif set == 'SCENE':
|
|
objs = prune_objects(self.inputs["Scene"].get_objects())
|
|
# Return pruned object list
|
|
return objs
|
|
|
|
# Get a list of unique objects used as either source or target
|
|
def get_unique_objects(self, type, for_auto_cage=False):
|
|
if for_auto_cage:
|
|
settings = self.get_settings()
|
|
if not settings['auto_cage']:
|
|
return []
|
|
objs = self.get_objects(type)
|
|
if for_auto_cage:
|
|
objs_cage = []
|
|
for obj in objs:
|
|
if len(obj) > 2 and obj[2]:
|
|
continue
|
|
else:
|
|
objs_cage.append([obj[0], settings['acage_expansion'], settings['acage_smooth']])
|
|
return objs_cage
|
|
return objs
|
|
|
|
view_orig = (
|
|
('ABV', "Above Surface", "Cast rays from above surface"),
|
|
('CAM', "Camera", "Cast rays from camera location"),
|
|
)
|
|
|
|
view_from: bpy.props.EnumProperty(name="View from", description="Ray casting origin (where applicable)", items=view_orig, default='ABV')
|
|
view_cam: bpy.props.PointerProperty(name="View camera", description="Camera to use for ray origins", type=bpy.types.Object)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_MeshSetting', "Settings")
|
|
self.inputs.new('BakeWrangler_Socket_Object', "Target")
|
|
self.inputs.new('BakeWrangler_Socket_Object', "Source")
|
|
self.inputs.new('BakeWrangler_Socket_Object', "Scene")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Mesh', "Mesh")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
row0 = layout.row(align=True)
|
|
row1 = row0.row(align=True)
|
|
row2 = row0.row(align=True)
|
|
|
|
row1.prop(self, "view_from", text="View")
|
|
row2.prop_search(self, "view_cam", bpy.data, "objects", text="", icon='CAMERA_DATA')
|
|
|
|
if self.view_from == 'CAM':
|
|
row2.enabled = True
|
|
else:
|
|
row2.enabled = False
|
|
|
|
|
|
# Baking node that holds all the settings for a type of bake 'pass'. Takes one or more mesh input nodes as input.
|
|
class BakeWrangler_Bake_Pass(BakeWrangler_Tree_Node, Node):
|
|
'''Baking pass node'''
|
|
bl_label = 'Bake Pass'
|
|
bl_width_default = 160
|
|
|
|
# Returns the most identifing string for the node
|
|
def get_name(self):
|
|
name = BakeWrangler_Tree_Node.get_name(self)
|
|
if self.bake_picked:
|
|
name += " (%s)" % (self.bake_picked)
|
|
return name
|
|
|
|
# Makes sure there is always one empty input socket at the bottom by adding and removing sockets
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Mesh', "Mesh")
|
|
|
|
# Update node label based on selected pass
|
|
def update_pass(self, context):
|
|
if self.bake_cat == 'PBR':
|
|
pass_enum = self.passes_pbr
|
|
pass_bake = self.bake_pbr
|
|
elif self.bake_cat == 'CORE':
|
|
pass_enum = self.passes_core
|
|
pass_bake = self.bake_core
|
|
else:
|
|
pass_enum = self.passes_wrang
|
|
pass_bake = self.bake_wrang
|
|
|
|
# Update picked value
|
|
self.bake_picked = pass_bake
|
|
|
|
if self.label == "":
|
|
pass_label = "Pass: "
|
|
elif ":" in self.label:
|
|
start, sep, end = self.label.rpartition(":")
|
|
pass_label = start + ": "
|
|
for pas in pass_enum:
|
|
if pas[0] == pass_bake:
|
|
self.label = pass_label + pas[1]
|
|
break
|
|
|
|
# Get Mesh node inputs
|
|
def get_inputs(self):
|
|
meshes = []
|
|
for input in self.inputs:
|
|
if input.bl_idname == 'BakeWrangler_Socket_Mesh':
|
|
mesh = get_input(input)
|
|
if mesh:
|
|
if mesh.bl_idname == 'BakeWrangler_Sort_Meshes':
|
|
for inpt in mesh.inputs:
|
|
if inpt.islinked() and inpt.valid:
|
|
meshes.append([mesh, inpt])
|
|
else:
|
|
meshes.append([mesh])
|
|
return meshes
|
|
|
|
# Try to get nodes settings from local or global node
|
|
def get_settings(self, validating=False):
|
|
pass_settings = {}
|
|
settings = sampsets = get_input(self.inputs["Settings"])
|
|
if not settings or settings.bl_idname == 'BakeWrangler_SampleSettings':
|
|
settings = self.id_data.get_pinned_settings("PassSettings")
|
|
# Handle sample settings if pass settings found
|
|
if settings:
|
|
if not sampsets or sampsets.bl_idname != 'BakeWrangler_SampleSettings':
|
|
sampsets = None
|
|
# See if the settings has a connected samples settings
|
|
if "Samples" in settings.inputs.keys():
|
|
sampsets = get_input(settings.inputs["Samples"])
|
|
if not sampsets:
|
|
# See if there is a pinned samples settings
|
|
sampsets = self.id_data.get_pinned_settings("SampleSettings")
|
|
if validating:
|
|
return settings
|
|
# Make it so having no sample settings node still works
|
|
if not sampsets:
|
|
pass_settings['bake_samples'] = self.bake_samples if self.bake_samples != 0 else 1
|
|
pass_settings['bake_threshold'] = 0.0
|
|
pass_settings['bake_usethresh'] = False
|
|
pass_settings['bake_timelimit'] = 0.0
|
|
else:
|
|
if not get_input(self.inputs["Settings"]) and self.bake_samples != 0:
|
|
pass_settings['bake_samples'] = self.bake_samples
|
|
pass_settings['bake_samples'] = sampsets.bake_samples if 'bake_samples' not in pass_settings else pass_settings['bake_samples']
|
|
pass_settings['bake_threshold'] = sampsets.bake_threshold
|
|
pass_settings['bake_usethresh'] = sampsets.bake_usethresh
|
|
pass_settings['bake_timelimit'] = sampsets.bake_timelimit
|
|
pass_settings['x_res'] = settings.res_bake_x
|
|
pass_settings['y_res'] = settings.res_bake_y
|
|
pass_settings['bake_device'] = settings.bake_device
|
|
pass_settings['interpolate'] = settings.interpolate
|
|
pass_settings['use_world'] = settings.use_world
|
|
pass_settings['the_world'] = settings.the_world
|
|
pass_settings['cpy_render'] = settings.cpy_render
|
|
pass_settings['cpy_from'] = settings.cpy_from
|
|
pass_settings['tiles'] = settings.use_tiles
|
|
pass_settings['tile_size'] = settings.render_tile
|
|
pass_settings['threads'] = settings.render_threads
|
|
pass_settings['use_bg_col'] = settings.use_bg_col
|
|
pass_settings['bg_color'] = settings.bg_color
|
|
pass_settings['bake_cat'] = self.bake_cat
|
|
pass_settings['bake_type'] = self.bake_picked
|
|
pass_settings['use_mask'] = self.use_mask
|
|
pass_settings['node_name'] = self.get_name()
|
|
pass_settings['norm_s'] = self.norm_space
|
|
pass_settings['norm_r'] = self.norm_R
|
|
pass_settings['norm_g'] = self.norm_G
|
|
pass_settings['norm_b'] = self.norm_B
|
|
pass_settings['multi_pass'] = self.multi_pass
|
|
pass_settings['multi_samp'] = self.multi_samp
|
|
pass_settings['multi_targ'] = self.multi_targ
|
|
pass_settings['multi_sorc'] = self.multi_sorc
|
|
pass_settings['bev_rad'] = self.bev_rad
|
|
pass_settings['bev_samp'] = self.bev_samp
|
|
pass_settings['cavity_samp'] = self.cavity_samp
|
|
pass_settings['cavity_dist'] = self.cavity_dist
|
|
pass_settings['cavity_gamma'] = self.cavity_gamma
|
|
pass_settings['cavity_edges'] = self.cavity_edges
|
|
pass_settings['curv_mid'] = self.curv_mid
|
|
pass_settings['curv_vex'] = self.curv_vex
|
|
pass_settings['curv_cav'] = self.curv_cav
|
|
pass_settings['curv_vex_max'] = self.curv_vex_max
|
|
pass_settings['curv_cav_min'] = self.curv_cav_min
|
|
pass_settings['osl_curv_dist'] = self.osl_curv_dist
|
|
pass_settings['osl_curv_samp'] = self.osl_curv_samp
|
|
pass_settings['osl_curv_cont'] = self.osl_curv_cont
|
|
pass_settings['osl_curv_srgb'] = False
|
|
pass_settings['osl_height_dist'] = self.osl_height_dist
|
|
pass_settings['osl_height_samp'] = self.osl_height_samp
|
|
pass_settings['osl_height_midl'] = self.osl_height_midl
|
|
pass_settings['osl_height_void'] = self.osl_height_void
|
|
pass_settings['vert_col'] = self.vert_col
|
|
pass_settings['influences'] = set()
|
|
pass_settings['aov_name'] = self.aov_name
|
|
pass_settings['aov_input'] = self.aov_input
|
|
pass_settings['use_material_vpcolor'] = self.use_material_vpcolor
|
|
pass_settings['osl_bentnorm_dist'] = self.osl_bentnorm_dist
|
|
pass_settings['osl_bentnorm_samp'] = self.osl_bentnorm_samp
|
|
|
|
if self.use_direct:
|
|
pass_settings['influences'].add('DIRECT')
|
|
if self.use_indirect:
|
|
pass_settings['influences'].add('INDIRECT')
|
|
if self.use_color:
|
|
pass_settings['influences'].add('COLOR')
|
|
if self.bake_picked == 'COMBINED':
|
|
if self.use_diffuse:
|
|
pass_settings['influences'].add('DIFFUSE')
|
|
if self.use_glossy:
|
|
pass_settings['influences'].add('GLOSSY')
|
|
if self.use_transmission:
|
|
pass_settings['influences'].add('TRANSMISSION')
|
|
#if self.use_ao:
|
|
# pass_settings['influences'].add('AO')
|
|
if self.use_emit:
|
|
pass_settings['influences'].add('EMIT')
|
|
if self.use_bg_col:
|
|
pass_settings['use_bg_col'] = self.use_bg_col
|
|
pass_settings['bg_color'] = self.bg_color
|
|
return pass_settings
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message(s).
|
|
def validate(self, is_primary=False):
|
|
valid = [True]
|
|
# Validate has Settings
|
|
self.update_pass(None)
|
|
if not self.get_settings(validating=True):
|
|
valid[0] = False
|
|
valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected pass settings found"])
|
|
# Validate inputs
|
|
has_valid_input = False
|
|
is_multires = (self.bake_cat == 'CORE' and self.bake_core == 'MULTIRES')
|
|
for input in self.inputs:
|
|
if input.bl_idname == 'BakeWrangler_Socket_Mesh' and input.islinked() and input.valid:
|
|
if self.bake_cat == 'PBR':
|
|
input_valid = get_input(input).validate(check_materials=True)
|
|
else:
|
|
input_valid = get_input(input).validate(multires=is_multires)
|
|
if not input_valid.pop(0):
|
|
valid[0] = False
|
|
else:
|
|
has_valid_input = True
|
|
if len(input_valid):
|
|
valid += input_valid
|
|
errs = len(valid)
|
|
if not has_valid_input and errs < 2:
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
|
|
# Validate outputs
|
|
if is_primary:
|
|
has_valid_output = False
|
|
for output in self.outputs:
|
|
if output.is_linked:
|
|
for link in gather_output_links(output):
|
|
if link.is_valid and link.to_socket.valid:
|
|
output_valid = link.to_node.validate()
|
|
if not output_valid.pop(0):
|
|
valid[0] = False
|
|
else:
|
|
has_valid_output = True
|
|
if len(output_valid):
|
|
valid += output_valid
|
|
if not has_valid_output and errs == len(valid):
|
|
valid[0] = False
|
|
valid.append([_print("Output error", node=self, ret=True), ": No valid outputs connected"])
|
|
# Validated
|
|
return valid
|
|
|
|
pass_cats = (
|
|
('PBR', "PBR", "Bake passes for PBR materials. Most require the material to use a Principled BSDF"),
|
|
('CORE', "Blender", "Standard bake passes normally found in blender. Passes with the same name in a different category will have different behavior"),
|
|
('WRANG', "Wrangler", "Additional passes provided in Bake Wrangler that aren't PBR specific"),
|
|
)
|
|
|
|
passes_pbr = (
|
|
('ALBEDO', "Albedo", "Surface color without lighting (Principled shader only)"),
|
|
('SUBSURFACE', "Subsurface", "Multiplies with the subsurface radius to determine distance light travels below the surface (Principled shader only)"),
|
|
('SUBRADIUS', "Subsurf Radius", "Distance light scatters below the surface, each channel is for that R/G/B color of light (a higher R value will mean red light travels deeper) (Principled shader only)"),
|
|
('SUBCOLOR', "Subsurf Color", "Base subsurface scattering color (Principled shader only)"),
|
|
('METALLIC', "Metallic", "Surface 'metalness' values (Principled shader only)"),
|
|
('SPECULAR', "Specular", "Surface dielectric specular reflection values (Princpled shader only)"),
|
|
('SPECTINT', "Specular Tint", "Tint of direct facing reflections (Note: dielectrics have colorless reflections, but this allows faking some effects) (Principled shader only)"),
|
|
('ROUGHNESS', "Roughness", "Surface roughness values with no other influences (Principled shader only)"),
|
|
('SMOOTHNESS', "Smoothness", "Surface inverted roughness values with no other influences (Principled shader only)"),
|
|
('ANISOTROPIC', "Anisotropic", "Amount of anisotropy for specular reflections (Principled shader only)"),
|
|
('ANISOROTATION', "Aniso Rotation", "Rotation direction of anisotropy (Principled shader only)"),
|
|
('SHEEN', "Sheen", "Amount of soft velvet like reflections near edges for cloth like materials (Principled shader only)"),
|
|
('SHEENTINT', "Sheen Tint", "Color mixed with white for sheen reflections (Principled shader only)"),
|
|
('CLEARCOAT', "Clearcoat", "Extra white specular layer on surface of material (Principled shader only)"),
|
|
('CLEARROUGH', "Clear Roughness", "Roughness values of the clearcoat (Principled shader only)"),
|
|
('TRANSIOR', "IOR", "Index of refraction used for transmission values (Principled shader only)"),
|
|
('TRANSMISSION', "Transmission", "Opacity values of surface with no other influences (Principled shader only)"),
|
|
('TRANSROUGH', "Trans Roughness", "Roughness values for light transmitted completely through the surface (Principled shader only)"),
|
|
('EMIT', "Emission", "Surface self emission color values with no other influences (Principled shader only)"),
|
|
('ALPHA', "Alpha", "Surface transparency values (Principled shader only)"),
|
|
('TEXNORM', "Texture Normals", "Surface normals influenced only by texture values (no geometry)"),
|
|
('CLEARNORM', "Clearcoat Normals", "Surface normals influenced only by the clearcoat values"),
|
|
('OBJNORM', "Geometry Normals", "Surface normals ignoring any influence from textures"),
|
|
('AOV', "AOV Node", "The color or value channel of a named AOV node in the materials"),
|
|
('BBNORM', "Billboard Normals", "Normals using target billboards rotation as tangent space"),
|
|
)
|
|
|
|
passes_core = (
|
|
('COMBINED', "Combined", "Combine multiple passes into a single bake"),
|
|
('AO', "Ambient Occlusion", "Surface self occlusion values"),
|
|
('SHADOW', "Shadow", "Shadow map"),
|
|
('NORMAL', "Normal", "Surface normals"),
|
|
('UV', "UV", "UV Layout"),
|
|
('ROUGHNESS', "Roughness", "Surface roughness values"),
|
|
('SMOOTHNESS', "Smoothness", "Surface inverted roughness values"),
|
|
('EMIT', "Emit", "Surface self emission color values"),
|
|
('ENVIRONMENT', "Environment", "Colors coming from the environment"),
|
|
('DIFFUSE', "Diffuse", "Colors of a surface generated by a diffuse shader"),
|
|
('GLOSSY', "Glossy", "Colors of a surface generated by a glossy shader"),
|
|
('TRANSMISSION', "Transmission", "Colors of light passing through a material"),
|
|
('MULTIRES', "Multiresolution", "Data from a multiresolution modifier"),
|
|
)
|
|
|
|
passes_wrang = (
|
|
('BEVMASK', "Bevel Mask", "Map of bevels where beveled areas will be baked in white"),
|
|
('BEVNORMEMIT', "Bevel Normals (Emit)", "Normal map with only bevel influences (can bake from one object to another, but inverted faces will be backwards)"),
|
|
('BEVNORMNORM', "Bevel Normals (Norm)", "Normal map with only bevel influences (does not work for baking from one object to another, but handles inverted faces)"),
|
|
('CAVITY', "Cavity/Edges", "Surface cavity occlusion or edges map"),
|
|
('CURVATURE', "Curvature", "Surface curvature map"),
|
|
#('OSL_CURV', "Curvature (OSL)", "OSL implementation of surface curvature map (CPU only)"),
|
|
('OSL_HEIGHT', "Height (OSL)", "Height map created by the distance between two surfaces (OSL shader only supports CPU)"),
|
|
('ISLANDID', "Island ID", "Map where each island of faces are baked in a different color"),
|
|
('MATID', "Material ID", "Map where each material is baked in a random solid color (based on their name)"),
|
|
('OBJCOL', "Object Color", "Each object is baked using its assigned viewport color"),
|
|
('WORLDPOS', "Position", "Areas of the object are baked with colors representing their position in the world"),
|
|
('THICKNESS', "Thickness", "Mesh thickness is baked from white (thin) to black (thick)"),
|
|
('VERTCOL', "Vertex Color", "Selected vertex colors are baked as the surface color"),
|
|
('OSL_BENTNORM', "Bent Normals (OSL)", "Bends normals based on ambient occlusion giving a directional bias"),
|
|
('MASKPASS', "UV Mask", "Black and white mask of pixels covered by UV islands"),
|
|
)
|
|
|
|
passes_all = passes_pbr + passes_core + passes_wrang
|
|
|
|
bake_has_influence = ['SUBSURFACE', 'TRANSMISSION', 'GLOSSY', 'DIFFUSE', 'COMBINED']
|
|
|
|
normal_spaces = (
|
|
('TANGENT', "Tangent", "Bake the normals in tangent space"),
|
|
('OBJECT', "Object", "Bake the normals in object space"),
|
|
)
|
|
|
|
normal_swizzle = (
|
|
('POS_X', "+X", ""),
|
|
('POS_Y', "+Y", ""),
|
|
('POS_Z', "+Z", ""),
|
|
('NEG_X', "-X", ""),
|
|
('NEG_Y', "-Y", ""),
|
|
('NEG_Z', "-Z", ""),
|
|
)
|
|
|
|
multires_subpasses = (
|
|
('NORMALS', "Normals", "Bake Normals"),
|
|
('DISPLACEMENT', "Displacement", "Bake Displacement"),
|
|
)
|
|
|
|
multires_sampling = (
|
|
('MAXIMUM', "Max to Min", "Bake the highest resolution down to the lowest"),
|
|
('FROMMOD', "Modifier Values", "Bake from the current render resolution to the current preview resolution"),
|
|
('CUSTOM', "Custom Values", "Choose custom values for the target and source resolutions"),
|
|
)
|
|
|
|
aov_inputs = (
|
|
('COL', "Color", "Use color data"),
|
|
('VAL', "Value", "Use value data"),
|
|
)
|
|
|
|
bake_result: bpy.props.PointerProperty(name="bake_result", description="Used internally by BW", type=bpy.types.Image)
|
|
mask_result: bpy.props.PointerProperty(name="mask_result", description="Used internally by BW", type=bpy.types.Image)
|
|
sbake_result: bpy.props.PointerProperty(name="sbake_result", description="Used internally by BW", type=bpy.types.Image)
|
|
smask_result: bpy.props.PointerProperty(name="smask_result", description="Used internally by BW", type=bpy.types.Image)
|
|
|
|
bake_cat: bpy.props.EnumProperty(name="Group", description="Category to select bake passes from", items=pass_cats, default='PBR', update=update_pass)
|
|
bake_pbr: bpy.props.EnumProperty(name="Pass", description="Type of PBR pass to bake", items=passes_pbr, default='ALBEDO', update=update_pass)
|
|
bake_core: bpy.props.EnumProperty(name="Pass", description="Type of Blender standard pass to bake", items=passes_core, default='COMBINED', update=update_pass)
|
|
bake_wrang: bpy.props.EnumProperty(name="Pass", description="Type of Wrangler pass to bake", items=passes_wrang, default='BEVMASK', update=update_pass)
|
|
bake_picked: bpy.props.EnumProperty(name="Picked", description="Selected bake type", items=passes_all)
|
|
bake_samples: bpy.props.IntProperty(name="Bake Samples", description="Number of samples to bake for each pixel. Use 25 to 50 samples for most bake types (AO may look better with more).\nQuality is gained by increaseing resolution rather than samples past that point", default=0, min=0)
|
|
|
|
adv_settings: bpy.props.BoolProperty(name="Advanced Settings", description="Show or hide advanced settings", default=False)
|
|
|
|
use_mask: bpy.props.BoolProperty(name="Use Masking", description="Generate a map of changed UV islands to use as a mask when updating pixel values. Allows layering of multiple passes onto a single image so long as they don't overlap", default=False)
|
|
use_direct: bpy.props.BoolProperty(name="Direct", description="Add direct lighting contribution", default=True)
|
|
use_indirect: bpy.props.BoolProperty(name="Indirect", description="Add indirect lighting contribution", default=True)
|
|
use_color: bpy.props.BoolProperty(name="Color", description="Color the pass", default=True)
|
|
use_diffuse: bpy.props.BoolProperty(name="Diffuse", description="Add diffuse contribution", default=True)
|
|
use_glossy: bpy.props.BoolProperty(name="Glossy", description="Add glossy contribution", default=True)
|
|
use_transmission: bpy.props.BoolProperty(name="Transmission", description="Add transmission contribution", default=True)
|
|
#use_subsurface: bpy.props.BoolProperty(name="Subsurface", description="Add subsurface contribution", default=True)
|
|
#use_ao: bpy.props.BoolProperty(name="Ambient Occlusion", description="Add ambient occlusion contribution", default=True)
|
|
use_emit: bpy.props.BoolProperty(name="Emit", description="Add emission contribution", default=True)
|
|
|
|
norm_space: bpy.props.EnumProperty(name="Space", description="Space to bake the normals in", items=normal_spaces, default='TANGENT')
|
|
norm_R: bpy.props.EnumProperty(name="R", description="Axis to bake in Red channel", items=normal_swizzle, default='POS_X')
|
|
norm_G: bpy.props.EnumProperty(name="G", description="Axis to bake in Green channel", items=normal_swizzle, default='POS_Y')
|
|
norm_B: bpy.props.EnumProperty(name="B", description="Axis to bake in Blue channel", items=normal_swizzle, default='POS_Z')
|
|
multi_pass: bpy.props.EnumProperty(name="Multires Type", description="Type of multiresolution pass to bake", items=multires_subpasses, default='NORMALS')
|
|
multi_samp: bpy.props.EnumProperty(name="Multires Method", description="Method to pick multiresolution source and target", items=multires_sampling, default='MAXIMUM')
|
|
multi_targ: bpy.props.IntProperty(name="Multires Target", description="Subdivision level for target of bake", default=0, min=0, soft_max=16)
|
|
multi_sorc: bpy.props.IntProperty(name="Multires Source", description="Subdivision level for source of bake", default=8, min=0, soft_max=16)
|
|
|
|
bev_rad: bpy.props.FloatProperty(name="Bevel Radius", description="Width of bevel on edges", default=0.05, min=0)
|
|
bev_samp: bpy.props.IntProperty(name="Bevel Samples", description="Number of samples to take (more gives greater accuracy at cost of time. A value of 4 works well in most cases and noise can be resolved by using more bake pass samples instead of increasing this value)", min=2, max=16, default=4)
|
|
cavity_samp: bpy.props.IntProperty(name="Cavity Over Samples", description="Number of samples to take (more gives a more accurate result but takes longer, increase bake pass samples to reduce noise before increasing this value)", default=16, min=1, max=128)
|
|
cavity_dist: bpy.props.FloatProperty(name="Cavity Sample Distance", description="How far away a face can be to contribute to the calculation (may need larger distances for larger objects)", default=0.4, step=1, min=0.0, unit='LENGTH')
|
|
cavity_gamma: bpy.props.FloatProperty(name="Cavity Gamma", description="Gamma transform to be performed on cavity values", default=1.0, step=1)
|
|
cavity_edges: bpy.props.BoolProperty(name="Edge Mode", description="Inverts the cavity map normals to find edges. If it's too dark, lower distance value", default=False)
|
|
curv_mid: bpy.props.FloatProperty(name="Curvature Flat", description="Value to assign to the mid-point between curves (flat area)", default=0.5, min=0.0, max=1.0)
|
|
curv_vex: bpy.props.FloatProperty(name="Curvature Convex", description="Value to assign to the sharpest (maximum) point of a convex curve", default=1.0, min=0.0, max=1.0)
|
|
curv_cav: bpy.props.FloatProperty(name="Curvature Concave", description="Value to assign to the sharpest (minimum) point of a concave curve", default=0.0, min=0.0, max=1.0)
|
|
curv_vex_max: bpy.props.FloatProperty(name="Curvature Convex Max", description="Convex value to consider the maximum curvature (higher values create a greater range)", default=0.2, min=0.0001, max=1.0)
|
|
curv_cav_min: bpy.props.FloatProperty(name="Curvature Concave Min", description="Concave value to consider the minimum curvature (lower values create a greater range)", default=0.8, min=0.0, max=0.9999)
|
|
osl_curv_dist: bpy.props.FloatProperty(name="Curvature Distance (OSL)", description="Distance to search for a neighboring surface", default=0.1, min=0.0)
|
|
osl_curv_samp: bpy.props.IntProperty(name="Curvature Samples (OSL)", description="Number of attempts to try finding a neighboring surface within the distance value", default=16, min=1)
|
|
osl_curv_cont: bpy.props.FloatProperty(name="Curvature Contrast (OSL)", description="Contrast level applied to curvature value", default=0.0, min=0.0)
|
|
osl_height_dist: bpy.props.FloatProperty(name="Height Distance", description="Maximum distance from source to look for a surface", default=0.1, min=0.0)
|
|
osl_height_samp: bpy.props.IntProperty(name="Height Samples", description="How many surfaces to consider for each point. Must be at least two but complex objects with faces close together may need more for correct results", default=2, min=2)
|
|
osl_height_midl: bpy.props.FloatProperty(name="Height Mid Level", description="Value to use as the middel level (neither high or low)", default=0.5, min=0.0, max=1.0)
|
|
osl_height_void: bpy.props.FloatVectorProperty(name="Height Void Color", description="Color to fill in areas where no height value could be found within the search distance", default=[0.5,0.5,0.5,1.0], size=4, subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10)
|
|
vert_col: bpy.props.StringProperty(name="Vertex Color Layer", description="Vertex color layer to bake from (leaving blank will use active)", default="")
|
|
use_bg_col: bpy.props.BoolProperty(name="BG Color", description="Background color for blank areas", default=False)
|
|
bg_color: bpy.props.FloatVectorProperty(name="BG Color", description="Background color used in blank areas", subtype='COLOR', soft_min=0.0, soft_max=1.0, step=10, default=[0.0,0.0,0.0,1.0], size=4)
|
|
aov_name: bpy.props.StringProperty(name="AOV Name", description="Name assigned to AOV node you want to bake from", default="")
|
|
aov_input: bpy.props.EnumProperty(name="AOV Source", description="Take data from either color or value input of AOV node", items=aov_inputs, default='COL')
|
|
use_material_vpcolor: bpy.props.BoolProperty(name="Use Viewport Color", description="Use the materials assigned viewport color instead of generating a random one based on its name", default=False)
|
|
use_subtraction: bpy.props.BoolProperty(name="Use Subtraction", description="Subtract the object normals from the material normals to isolate them. This results in correct tangent normal rotations but may not be as clean a result", default=True)
|
|
osl_bentnorm_dist: bpy.props.FloatProperty(name="Bent Normals Distance", description="Maximum distance for a surface to contribute to ambient occlusion", default=1.0, min=0.001)
|
|
osl_bentnorm_samp: bpy.props.IntProperty(name="Height Samples", description="Number of ambient light samples to take for each surface point", default=8, min=1)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Set label to pass
|
|
self.update_pass(context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_PassSetting', "Settings")
|
|
self.inputs.new('BakeWrangler_Socket_Mesh', "Mesh")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Color', "Color")
|
|
# Prefs
|
|
self.adv_settings = _prefs("def_show_adv")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
colnode = layout.column(align=False)
|
|
|
|
colpass = colnode.column(align=True)
|
|
colpass.prop(self, "bake_cat")
|
|
if self.bake_cat == 'PBR':
|
|
colpass.prop(self, "bake_pbr")
|
|
elif self.bake_cat == 'CORE':
|
|
colpass.prop(self, "bake_core")
|
|
else:
|
|
colpass.prop(self, "bake_wrang")
|
|
|
|
advrow = colnode.row()
|
|
advrow.alignment = 'LEFT'
|
|
|
|
if not self.adv_settings:
|
|
adv = advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_RIGHT", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
else:
|
|
adv = advrow.prop(self, "adv_settings", icon="DISCLOSURE_TRI_DOWN", emboss=False, text="Advanced:")
|
|
advrow.separator()
|
|
|
|
col = colnode.column(align=True)
|
|
col.prop(self, "use_mask", toggle=True)
|
|
bg_col = col.row(align=True)
|
|
bg_col.prop(self, "use_bg_col", toggle=True)
|
|
if self.use_bg_col:
|
|
bg_col.prop(self, "bg_color", toggle=True, text="")
|
|
|
|
splitopt = colnode.split(factor=0.5)
|
|
colopttxt = splitopt.column(align=True)
|
|
colopttxt.alignment = 'RIGHT'
|
|
coloptval = splitopt.column(align=True)
|
|
|
|
# Additional options for PBR passes
|
|
if self.bake_cat == 'PBR':
|
|
# AOV ouput
|
|
if self.bake_pbr == 'AOV':
|
|
colopttxt.label(text="Name:")
|
|
colopttxt.label(text="Input:")
|
|
|
|
coloptval.prop(self, "aov_name", text="")
|
|
coloptval.prop(self, "aov_input", text="")
|
|
# Additional options for 'Wrangler' passes
|
|
if self.bake_cat == 'WRANG':
|
|
# Bevel mask
|
|
if self.bake_wrang in ['BEVMASK', 'BEVNORMEMIT', 'BEVNORMNORM']:
|
|
colopttxt.label(text="Samples:")
|
|
colopttxt.label(text="Radius:")
|
|
|
|
coloptval.prop(self, "bev_samp", text="")
|
|
coloptval.prop(self, "bev_rad", text="")
|
|
elif self.bake_wrang == 'CAVITY':
|
|
colopttxt.label(text="Edge Mode:")
|
|
colopttxt.label(text="Over Samples:")
|
|
colopttxt.label(text="Distance:")
|
|
colopttxt.label(text="Gamma:")
|
|
|
|
coloptval.prop(self, "cavity_edges", text="")
|
|
coloptval.prop(self, "cavity_samp", text="")
|
|
coloptval.prop(self, "cavity_dist", text="")
|
|
coloptval.prop(self, "cavity_gamma", text="")
|
|
elif self.bake_wrang == 'THICKNESS':
|
|
colopttxt.label(text="Over Samples:")
|
|
colopttxt.label(text="Distance:")
|
|
|
|
coloptval.prop(self, "cavity_samp", text="")
|
|
coloptval.prop(self, "cavity_dist", text="")
|
|
elif self.bake_wrang == 'CURVATURE':
|
|
colopttxt.label(text="Flat:")
|
|
colopttxt.label(text="Convex:")
|
|
colopttxt.label(text="Convex Max:")
|
|
colopttxt.label(text="Concave:")
|
|
colopttxt.label(text="Concave Min:")
|
|
|
|
coloptval.prop(self, "curv_mid", text="")
|
|
coloptval.prop(self, "curv_vex", text="")
|
|
coloptval.prop(self, "curv_vex_max", text="")
|
|
coloptval.prop(self, "curv_cav", text="")
|
|
coloptval.prop(self, "curv_cav_min", text="")
|
|
elif self.bake_wrang == 'OSL_CURV':
|
|
colopttxt.label(text="Distance:")
|
|
colopttxt.label(text="Samples:")
|
|
colopttxt.label(text="Contrast:")
|
|
|
|
coloptval.prop(self, "osl_curv_dist", text="")
|
|
coloptval.prop(self, "osl_curv_samp", text="")
|
|
coloptval.prop(self, "osl_curv_cont", text="")
|
|
elif self.bake_wrang == 'OSL_HEIGHT':
|
|
colopttxt.label(text="Distance:")
|
|
colopttxt.label(text="Samples:")
|
|
colopttxt.label(text="Mid Level:")
|
|
colopttxt.label(text="Void Color:")
|
|
|
|
coloptval.prop(self, "osl_height_dist", text="")
|
|
coloptval.prop(self, "osl_height_samp", text="")
|
|
coloptval.prop(self, "osl_height_midl", text="")
|
|
coloptval.prop(self, "osl_height_void", text="")
|
|
elif self.bake_wrang == 'VERTCOL':
|
|
colopttxt.label(text="Layer:")
|
|
|
|
coloptval.prop(self, "vert_col", text="")
|
|
elif self.bake_wrang == 'MATID':
|
|
colnode.prop(self, "use_material_vpcolor")
|
|
elif self.bake_wrang == 'OSL_BENTNORM':
|
|
colopttxt.label(text="Distance:")
|
|
colopttxt.label(text="Samples:")
|
|
|
|
coloptval.prop(self, "osl_bentnorm_dist", text="")
|
|
coloptval.prop(self, "osl_bentnorm_samp", text="")
|
|
|
|
# Additional options for 'core' passes
|
|
if self.bake_cat == 'CORE':
|
|
# Multires
|
|
if self.bake_core == 'MULTIRES':
|
|
colopttxt.label(text="Type:")
|
|
colopttxt.label(text="Method:")
|
|
|
|
coloptval.prop(self, "multi_pass", text="")
|
|
coloptval.prop(self, "multi_samp", text="")
|
|
if self.multi_samp == 'CUSTOM':
|
|
colopttxt.label(text="Target Divs:")
|
|
colopttxt.label(text="Source Divs:")
|
|
|
|
coloptval.prop(self, "multi_targ", text="")
|
|
coloptval.prop(self, "multi_sorc", text="")
|
|
elif self.bake_core in self.bake_has_influence:
|
|
row = colnode.row(align=True)
|
|
row.use_property_split = False
|
|
row.prop(self, "use_direct", toggle=True)
|
|
row.prop(self, "use_indirect", toggle=True)
|
|
if self.bake_core != 'COMBINED':
|
|
row.prop(self, "use_color", toggle=True)
|
|
else:
|
|
col = colnode.column(align=True)
|
|
col.prop(self, "use_diffuse")
|
|
col.prop(self, "use_glossy")
|
|
col.prop(self, "use_transmission")
|
|
#col.prop(self, "use_subsurface")
|
|
#col.prop(self, "use_ao")
|
|
col.prop(self, "use_emit")
|
|
# Any normal map passes
|
|
if (self.bake_cat == 'PBR' and self.bake_pbr in ['TEXNORM', 'CLEARNORM', 'OBJNORM', 'BBNORM']) \
|
|
or (self.bake_cat == 'CORE' and self.bake_core in ['NORMAL']) \
|
|
or (self.bake_cat == 'WRANG' and self.bake_wrang in ['BEVNORMEMIT', 'BEVNORMNORM', 'OSL_BENTNORM']):
|
|
if self.bake_picked == 'TEXNORM':
|
|
colnode.prop(self, "use_subtraction")
|
|
colopttxt.label(text="Space:")
|
|
colopttxt.label(text="R:")
|
|
colopttxt.label(text="G:")
|
|
colopttxt.label(text="B:")
|
|
|
|
coloptval.prop(self, "norm_space", text="")
|
|
coloptval.prop(self, "norm_R", text="")
|
|
coloptval.prop(self, "norm_G", text="")
|
|
coloptval.prop(self, "norm_B", text="")
|
|
|
|
|
|
|
|
# The channel map combines inputs by mapping them to RGBA channels of the output and sits between passes and output
|
|
# images
|
|
class BakeWrangler_Channel_Map(BakeWrangler_Tree_Node, Node):
|
|
'''Channel map node'''
|
|
bl_label = 'Channel Map'
|
|
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
return BakeWrangler_Tree_Node.validate(self, True)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Color', "Color")
|
|
self.inputs.new('BakeWrangler_Socket_ChanMap', "Red")
|
|
self.inputs.new('BakeWrangler_Socket_ChanMap', "Green")
|
|
self.inputs.new('BakeWrangler_Socket_ChanMap', "Blue")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Color', "Color")
|
|
|
|
|
|
|
|
# Mix RGB via a factor and operation
|
|
class BakeWrangler_Post_MixRGB(BakeWrangler_Tree_Node, Node):
|
|
'''Mix RGB node'''
|
|
bl_label = 'Mix RGB'
|
|
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
return BakeWrangler_Tree_Node.validate(self, True, True)
|
|
|
|
ops = (
|
|
('MIX', "Mix", ""),
|
|
('ADD', "Add", ""),
|
|
('SUBTRACT', "Subtract", ""),
|
|
('MULTIPLY', "Multiply", ""),
|
|
('DIVIDE', "Divide", ""),
|
|
('OVERLAY', "Overlay", ""),
|
|
)
|
|
|
|
op: bpy.props.EnumProperty(name="Operator", description="Mathematical operation to perform", items=ops, default='MIX')
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Float', "Fac")
|
|
self.inputs.new('BakeWrangler_Socket_Color', "Color1")
|
|
self.inputs.new('BakeWrangler_Socket_Color', "Color2")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Color', "Color")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
layout.prop(self, "op", text="")
|
|
|
|
|
|
|
|
# Split RGB ouput into channels
|
|
class BakeWrangler_Post_SplitRGB(BakeWrangler_Tree_Node, Node):
|
|
'''Split RGB node'''
|
|
bl_label = 'Split RGB'
|
|
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
return BakeWrangler_Tree_Node.validate(self, True)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Color', "Color")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Float', "Red")
|
|
self.outputs.new('BakeWrangler_Socket_Float', "Green")
|
|
self.outputs.new('BakeWrangler_Socket_Float', "Blue")
|
|
|
|
|
|
|
|
# Join channels into single RGB color
|
|
class BakeWrangler_Post_JoinRGB(BakeWrangler_Tree_Node, Node):
|
|
'''Join RGB node'''
|
|
bl_label = 'Join RGB'
|
|
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
return BakeWrangler_Tree_Node.validate(self, True, True)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Float', "Red")
|
|
self.inputs.new('BakeWrangler_Socket_Float', "Green")
|
|
self.inputs.new('BakeWrangler_Socket_Float', "Blue")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Color', "Color")
|
|
|
|
|
|
|
|
# Perform math functions on image data
|
|
class BakeWrangler_Post_Math(BakeWrangler_Tree_Node, Node):
|
|
'''Math node'''
|
|
bl_label = 'Math'
|
|
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
return BakeWrangler_Tree_Node.validate(self, True, True)
|
|
|
|
# Disable inputs based on function
|
|
def chg_op(self, context):
|
|
if self.op in ['FLOOR', 'CEIL']:
|
|
self.inputs[1].enabled = False
|
|
else:
|
|
self.inputs[1].enabled = True
|
|
|
|
ops = (
|
|
('ADD', "Add", "A + B"),
|
|
('SUBTRACT', "Subtract", "A - B"),
|
|
('MULTIPLY', "Multiply", "A * B"),
|
|
('DIVIDE', "Divide", "A / B"),
|
|
|
|
('POWER', "Power", "A^B"),
|
|
('LOGARITHM', "Logarithm", "Log A base B"),
|
|
|
|
('FLOOR', "Floor", "Largest integer <= A"),
|
|
('CEIL', "Ceil", "Smallest integer >= A"),
|
|
('MODULO', "Modulo", "Mod A / B"),
|
|
)
|
|
|
|
op: bpy.props.EnumProperty(name="Operator", description="Mathematical operation to perform", items=ops, update=chg_op, default='ADD')
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Float', "Value", identifier="0")
|
|
self.inputs.new('BakeWrangler_Socket_Float', "Value", identifier="1")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Float', "Value")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
layout.menu("BW_MT_math_ops", text=layout.enum_item_name(self, "op", self.op))
|
|
|
|
# Menu displayed in the math node to select function
|
|
#
|
|
# Found out headers can be created in enums by setting all but name to empty string
|
|
# eg ("", "ColumnName", "") - So this could probably be replaced with that, but w/e
|
|
#
|
|
class BakeWrangler_Post_Math_OpMenu(bpy.types.Menu):
|
|
bl_idname = "BW_MT_math_ops"
|
|
bl_label = "Ops"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout.row()
|
|
layout.alignment = 'LEFT'
|
|
col1 = layout.column()
|
|
col1.alignment = 'LEFT'
|
|
col1.label(text="Functions")
|
|
col1.separator()
|
|
for op in context.node.ops:
|
|
if op[0] in ['ADD', 'SUBTRACT', 'MULTIPLY', 'DIVIDE']:
|
|
itm = col1.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1])
|
|
BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name)
|
|
col1.separator()
|
|
for op in context.node.ops:
|
|
if op[0] in ['POWER', 'LOGARITHM']:
|
|
itm = col1.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1])
|
|
BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name)
|
|
col2 = layout.column()
|
|
col2.alignment = 'LEFT'
|
|
col2.label(text="Rounding")
|
|
col2.separator()
|
|
for op in context.node.ops:
|
|
if op[0] in ['FLOOR', 'CEIL', 'MODULO']:
|
|
itm = col2.operator("bake_wrangler.pick_menu_enum", icon='NONE', text=op[1])
|
|
BakeWrangler_Operator_PickMenuEnum.set_props(itm, op[0], op[2], "op", context.node.name, context.node.id_data.name)
|
|
|
|
|
|
|
|
# Apply gamma transform to color
|
|
class BakeWrangler_Post_Gamma(BakeWrangler_Tree_Node, Node):
|
|
'''Gamma node'''
|
|
bl_label = 'Gamma'
|
|
|
|
def update_inputs(self):
|
|
pass
|
|
|
|
def validate(self):
|
|
return BakeWrangler_Tree_Node.validate(self, True)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Color', "Color")
|
|
self.inputs.new('BakeWrangler_Socket_Float', "Gamma")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Color', "Color")
|
|
|
|
|
|
|
|
# Output node that specifies the path to a file where a bake should be saved along with size and format information.
|
|
# Takes input from the outputs of a bake pass node. Connecting multiple inputs will cause higher position inputs to
|
|
# be over written by lower ones. Eg: Having a color input and an R input would cause the R channel of the color data
|
|
# to be overwritten by the data connected tot he R input.
|
|
class BakeWrangler_Output_Image_Path(BakeWrangler_Tree_Node, Node):
|
|
'''Output image path node'''
|
|
bl_label = 'Output Image Path'
|
|
bl_width_default = 160
|
|
|
|
# Returns the most identifying string for the node
|
|
def get_name(self):
|
|
name = BakeWrangler_Tree_Node.get_name(self)
|
|
if self.inputs['Split Output'].get_name():
|
|
name += " (%s)" % (self.inputs['Split Output'].get_name())
|
|
return name
|
|
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Color', "Color", {'Alpha':'BakeWrangler_Socket_ChanMap'})
|
|
settings = self.get_settings()
|
|
if settings:
|
|
if settings['img_color_mode'] != 'RGBA':
|
|
for input in self.inputs:
|
|
if input.name == 'Alpha' and input.enabled:
|
|
input.enabled = False
|
|
else:
|
|
idx = 0
|
|
for input in self.inputs:
|
|
if input.name == 'Color':
|
|
if input.is_linked:
|
|
self.inputs[idx+1].enabled = True
|
|
else:
|
|
self.inputs[idx+1].enabled = False
|
|
idx += 1
|
|
|
|
# Try to get nodes settings from local or global node
|
|
def get_settings(self, validating=False):
|
|
settings = None
|
|
if 'Settings' in self.inputs:
|
|
settings = get_input(self.inputs["Settings"])
|
|
if not settings:
|
|
settings = self.id_data.get_pinned_settings("OutputSettings")
|
|
if validating:
|
|
img_non_color = None
|
|
if 'Non-Color' in bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys():
|
|
img_non_color = 'Non-Color'
|
|
elif _prefs('img_non_color') is not None and (len(bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys()) - 1) >= int(_prefs('img_non_color')):
|
|
img_non_color = bpy.types.ColorManagedInputColorspaceSettings.bl_rna.properties['name'].enum_items.keys()[_prefs('img_non_color')]
|
|
settings.img_non_color = img_non_color
|
|
return settings, img_non_color
|
|
if not settings:
|
|
# No local or pinned settings
|
|
return None
|
|
output_settings = {}
|
|
output_settings['img_non_color'] = settings.img_non_color
|
|
output_settings['img_xres'] = settings.img_xres
|
|
output_settings['img_yres'] = settings.img_yres
|
|
output_settings['img_clear'] = settings.img_clear
|
|
output_settings['fast_aa'] = settings.fast_aa
|
|
output_settings['fast_aa_lvl'] = settings.fast_aa_lvl
|
|
output_settings['img_type'] = settings.img_type
|
|
output_settings['img_udim'] = settings.img_udim
|
|
output_settings['img_color_space'] = settings.img_color_space
|
|
output_settings['img_use_float'] = settings.img_use_float
|
|
output_settings['img_jpeg2k_cinema'] = settings.img_jpeg2k_cinema
|
|
output_settings['img_jpeg2k_cinema48'] = settings.img_jpeg2k_cinema48
|
|
output_settings['img_jpeg2k_ycc'] = settings.img_jpeg2k_ycc
|
|
output_settings['img_dpx_log'] = settings.img_dpx_log
|
|
output_settings['img_openexr_zbuff'] = settings.img_openexr_zbuff
|
|
output_settings['marginer'] = settings.marginer
|
|
output_settings['marginer_size'] = settings.marginer_size
|
|
output_settings['marginer_fill'] = settings.marginer_fill
|
|
|
|
output_settings['img_color_mode'] = None
|
|
if settings.img_type in ['BMP', 'JPEG', 'CINEON', 'HDR']:
|
|
output_settings['img_color_mode'] = settings.img_color_mode_noalpha
|
|
elif settings.img_type in ['IRIS', 'PNG', 'JPEG2000', 'TARGA', 'TARGA_RAW', 'DPX', 'OPEN_EXR_MULTILAYER', 'OPEN_EXR', 'TIFF']:
|
|
output_settings['img_color_mode'] = settings.img_color_mode
|
|
|
|
output_settings['img_color_depth'] = None
|
|
if settings.img_type in ['PNG', 'TIFF']:
|
|
output_settings['img_color_depth'] = settings.img_color_depth_8_16
|
|
elif settings.img_type == 'JPEG2000':
|
|
output_settings['img_color_depth'] = settings.img_color_depth_8_12_16
|
|
elif settings.img_type == 'DPX':
|
|
output_settings['img_color_depth'] = settings.img_color_depth_8_10_12_16
|
|
elif settings.img_type in ['OPEN_EXR_MULTILAYER', 'OPEN_EXR']:
|
|
output_settings['img_color_depth'] = settings.img_color_depth_16_32
|
|
|
|
output_settings['img_quality'] = None
|
|
if settings.img_type == 'PNG':
|
|
output_settings['img_quality'] = settings.img_compression
|
|
elif settings.img_type in ['JPEG', 'JPEG2000']:
|
|
output_settings['img_quality'] = settings.img_quality
|
|
|
|
output_settings['img_codec'] = None
|
|
if settings.img_type == 'JPEG2000':
|
|
output_settings['img_codec'] = settings.img_codec_jpeg2k
|
|
elif settings.img_type in ['OPEN_EXR', 'OPEN_EXR_MULTILAYER']:
|
|
output_settings['img_codec'] = settings.img_codec_openexr
|
|
elif settings.img_type == 'TIFF':
|
|
output_settings['img_codec'] = settings.img_codec_tiff
|
|
|
|
output_settings['img_path'] = self.get_full_path()
|
|
|
|
return output_settings
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message(s).
|
|
def validate(self, is_primary=False):
|
|
valid = [True]
|
|
# Validate has Settings
|
|
settings, none_color = self.get_settings(True)
|
|
if not settings:
|
|
valid[0] = False
|
|
valid.append([_print("Settings missing", node=self, ret=True), ": No pinned or connected output settings found"])
|
|
return valid
|
|
if not none_color:
|
|
valid[0] = False
|
|
valid.append([_print("Non-standard color spaces", node=self, ret=True), ": Please set up your color space in addon preferences"])
|
|
return valid
|
|
# Validate inputs
|
|
has_valid_input = False
|
|
for input in self.inputs:
|
|
if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid:
|
|
if not is_primary:
|
|
has_valid_input = True
|
|
break
|
|
else:
|
|
input_valid = get_input(input).validate()
|
|
valid[0] = input_valid.pop(0)
|
|
if valid[0]:
|
|
has_valid_input = True
|
|
valid += input_valid
|
|
errs = len(valid)
|
|
if not has_valid_input and errs < 2:
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
|
|
# Validate file path
|
|
self.img_path = self.get_full_path()
|
|
if not os.path.isdir(os.path.abspath(self.img_path)):
|
|
# Try creating the path if enabled in prefs
|
|
if _prefs("make_dirs") and not os.path.exists(os.path.abspath(self.img_path)):
|
|
try:
|
|
os.makedirs(os.path.abspath(self.img_path))
|
|
except OSError as err:
|
|
valid[0] = False
|
|
valid.append([_print("Path error", node=self, ret=True), ": Trying to create path at '%s'" % (err.strerror)])
|
|
return valid
|
|
else:
|
|
valid[0] = False
|
|
valid.append([_print("Path error", node=self, ret=True), ": Invalid path '%s'" % (os.path.abspath(self.img_path))])
|
|
return valid
|
|
# Check if there is read/write access to the file/directory
|
|
settings = self.get_settings()
|
|
file_path = os.path.join(os.path.abspath(self.img_path), self.name_with_ext(settings['img_type']))
|
|
if os.path.exists(file_path):
|
|
if os.path.isfile(file_path):
|
|
# It exists so try to open it r/w
|
|
try:
|
|
file = open(file_path, "a")
|
|
except OSError as err:
|
|
valid[0] = False
|
|
valid.append([_print("File error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)])
|
|
else:
|
|
# See if it can be read as an image
|
|
file.close()
|
|
file_img = bpy.data.images.load(file_path)
|
|
if not len(file_img.pixels):
|
|
valid[0] = False
|
|
valid.append([_print("File error", node=self, ret=True), ": File exists but doesn't seem to be a known image format"])
|
|
bpy.data.images.remove(file_img)
|
|
else:
|
|
# It exists but isn't a file
|
|
valid[0] = False
|
|
valid.append([_print("File error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)])
|
|
else:
|
|
# See if it can be created
|
|
try:
|
|
file = open(file_path, "a")
|
|
except OSError as err:
|
|
valid[0] = False
|
|
valid.append([_print("File error", node=self, ret=True), ": %s trying to create file at '%s'" % (err.strerror, file_path)])
|
|
else:
|
|
file.close()
|
|
os.remove(file_path)
|
|
# Validated
|
|
return valid
|
|
|
|
# Get full path, removing any relative references
|
|
def get_full_path(self):
|
|
return self.inputs["Split Output"].get_full_path()
|
|
|
|
# Return the file name with the correct image type extension (unless it has an existing unknown extension)
|
|
def name_with_ext(self, suffix=""):
|
|
settings = self.get_settings()
|
|
return self.inputs["Split Output"].name_with_ext(suffix, settings['img_type'])
|
|
|
|
# Return frame ranges or padding or animated seed if set, otherwise empty list, None or False
|
|
def frame_range(self, padding=False, animated=False):
|
|
return self.inputs["Split Output"].frame_range(padding, animated)
|
|
|
|
# Return a dict of format settings
|
|
def get_format(self):
|
|
format = {}
|
|
for prop in self.rna_type.properties.keys():
|
|
format[prop] = getattr(self, prop)
|
|
return format
|
|
|
|
# Return a dict of output files with their connected input
|
|
def get_output_files(self):
|
|
output_files = {}
|
|
for input in self.inputs:
|
|
if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid:
|
|
output_files[self.name_with_ext(suffix=input.suffix)] = input
|
|
return output_files
|
|
|
|
# Get list of mesh objects from the split output socket
|
|
def get_split_objects(self):
|
|
objs = []
|
|
split = self.inputs["Split Output"].get_split()
|
|
if split and len(split):
|
|
for splt in split:
|
|
if splt.bl_idname == 'BakeWrangler_Bake_Mesh':
|
|
objs += prune_objects(splt.inputs["Target"].get_objects(only_mesh=True))
|
|
elif splt.bl_idname == 'BakeWrangler_Input_ObjectList':
|
|
objs += prune_objects(splt.get_objects(only_mesh=True))
|
|
elif splt.bl_idname == 'BakeWrangler_Bake_Material':
|
|
objs += splt.get_materials()
|
|
elif splt.bl_idname == 'BakeWrangler_Sort_Meshes':
|
|
sobj = []
|
|
for inpt in splt.inputs:
|
|
if inpt.islinked() and inpt.valid:
|
|
if get_input(inpt).bl_idname == 'BakeWrangler_Bake_Material':
|
|
for mat in get_input(inpt).get_materials():
|
|
sobj += [[mat]]
|
|
sobj += splt.get_objects('TARGET', inpt)
|
|
for grp in sobj:
|
|
grp[0].append(grp[3])
|
|
objs.append(grp[0])
|
|
objs = prune_objects(objs)
|
|
if len(objs): return objs
|
|
else: return None
|
|
|
|
# Get a list of unique objects used as either source or target
|
|
def get_unique_objects(self, type, for_auto_cage=False):
|
|
def get_meshes(socket, meshes):
|
|
pas = get_input(socket)
|
|
if pas:
|
|
if pas.bl_idname == 'BakeWrangler_Bake_Pass':
|
|
meshes += pas.get_inputs()
|
|
else:
|
|
for input in pas.inputs:
|
|
get_meshes(input, meshes)
|
|
meshes = []
|
|
for input in self.inputs:
|
|
if input.name in ["Color", "Alpha"]:
|
|
get_meshes(input, meshes)
|
|
objs = []
|
|
for mesh in meshes:
|
|
if len(mesh) > 1:
|
|
objs += mesh[0].get_unique_objects(type, for_auto_cage, mesh[1])
|
|
else:
|
|
objs += mesh[0].get_unique_objects(type, for_auto_cage)
|
|
objs = prune_objects(objs)
|
|
if for_auto_cage:
|
|
return objs
|
|
return objs
|
|
|
|
img_ext = (
|
|
('BMP', ".bmp"),
|
|
('IRIS', ".rgb"),
|
|
('PNG', ".png"),
|
|
('JPEG', ".jpg"),
|
|
('JPEG2000', ".jp2"),
|
|
('TARGA', ".tga"),
|
|
('TARGA_RAW', ".tga"),
|
|
('CINEON', ".cin"),
|
|
('DPX', ".dpx"),
|
|
('OPEN_EXR_MULTILAYER', ".exr"),
|
|
('OPEN_EXR', ".exr"),
|
|
('HDR', ".hdr"),
|
|
('TIFF', ".tif"),
|
|
)
|
|
|
|
# Core settings
|
|
img_path: bpy.props.StringProperty(name="Output Path", description="Path to save image in", default="", subtype='DIR_PATH')
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_SplitOutput', "Split Output")
|
|
self.inputs.new('BakeWrangler_Socket_OutputSetting', "Settings")
|
|
self.inputs.new('BakeWrangler_Socket_Color', "Color")
|
|
self.inputs.new('BakeWrangler_Socket_ChanMap', "Alpha")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Bake', "Bake")
|
|
# Prefs
|
|
self.inputs['Split Output'].disp_path = _prefs("def_outpath")
|
|
self.inputs['Split Output'].img_name = _prefs("def_outname")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
pass
|
|
#colnode = layout.column(align=False)
|
|
#colpath = colnode.column(align=True)
|
|
#colpath.prop(self, "disp_path", text="")
|
|
#colpath.prop(self, "img_name", text="")
|
|
|
|
|
|
class BakeWrangler_Output_Vertex_Cols(BakeWrangler_Tree_Node, Node):
|
|
'''Output vertex colors node'''
|
|
bl_label = 'Output Vertex Colors'
|
|
bl_width_default = 160
|
|
|
|
vert_files = []
|
|
|
|
# Returns the most identifying string for the node
|
|
def get_name(self):
|
|
name = BakeWrangler_Tree_Node.get_name(self)
|
|
return name
|
|
|
|
# Make sure an empty input is always at the bottom
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Color', "Color")
|
|
|
|
# All objects are split objects? We don't use this
|
|
def get_split_objects(self):
|
|
return None
|
|
|
|
# Image settings are mostly irrelevant here, mostly static values will be set
|
|
def get_settings(self, validating=False):
|
|
output_settings = {}
|
|
output_settings['vcol'] = True
|
|
output_settings['vcol_type'] = self.vcol_type
|
|
output_settings['vcol_domain'] = self.vcol_domain
|
|
output_settings['img_udim'] = False
|
|
output_settings['marginer'] = False
|
|
output_settings['marginer_size'] = 0
|
|
output_settings['marginer_fill'] = False
|
|
output_settings['img_color_mode'] = None
|
|
output_settings['img_color_depth'] = None
|
|
output_settings['img_path'] = None
|
|
return output_settings
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message(s).
|
|
def validate(self, is_primary=False):
|
|
valid = [True]
|
|
# Validate inputs
|
|
has_valid_input = False
|
|
for input in self.inputs:
|
|
if input.bl_idname == 'BakeWrangler_Socket_Color' and input.islinked() and input.valid:
|
|
if not input.suffix or not len(input.suffix):
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": Connected vertex output requires valid name for data"])
|
|
if not is_primary:
|
|
has_valid_input = True
|
|
break
|
|
else:
|
|
input_valid = get_input(input).validate()
|
|
valid[0] = input_valid.pop(0)
|
|
if valid[0]:
|
|
has_valid_input = True
|
|
valid += input_valid
|
|
errs = len(valid)
|
|
if not has_valid_input and errs < 2:
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
|
|
return valid
|
|
|
|
vcol_domains = (
|
|
('POINT', "Vertex", "Vertex"),
|
|
('CORNER', "Face Corner", "Face Corner"),
|
|
)
|
|
|
|
vcol_types = (
|
|
('FLOAT_COLOR', "Color", "32-bit floating point values"),
|
|
('BYTE_COLOR', "Byte Color", "8-bit integer values"),
|
|
)
|
|
|
|
# Core settings
|
|
vcol_domain: bpy.props.EnumProperty(name="Domain", description="Type of element the data is stored on", items=vcol_domains, default='POINT')
|
|
vcol_type: bpy.props.EnumProperty(name="Data Type", description="Type of data stored in element", items=vcol_types, default='FLOAT_COLOR')
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
# Sockets IN
|
|
self.inputs.new('BakeWrangler_Socket_Color', "Color")
|
|
# Sockets OUT
|
|
self.outputs.new('BakeWrangler_Socket_Bake', "Bake")
|
|
# Prefs
|
|
|
|
def draw_buttons(self, context, layout):
|
|
row = layout.row(align=True)
|
|
col = layout.column(align=True)
|
|
if not len(self.vert_files):
|
|
row.operator("bake_wrangler.dummy_vcol", icon='CHECKMARK', text="Apply")
|
|
row.operator("bake_wrangler.dummy_vcol", icon='PANEL_CLOSE', text="Discard")
|
|
else:
|
|
op1 = row.operator("bake_wrangler.apply_vertcols", icon='CHECKMARK', text="Apply")
|
|
op1.tree = self.id_data.name
|
|
op1.node = self.name
|
|
op2 = row.operator("bake_wrangler.discard_vertcols", icon='PANEL_CLOSE', text="Discard")
|
|
op2.tree = self.id_data.name
|
|
op2.node = self.name
|
|
|
|
col.prop(self, "vcol_domain", text="")
|
|
col.prop(self, "vcol_type", text="")
|
|
|
|
|
|
# Output controller node provides batch execution of multiple connected bake passes.
|
|
class BakeWrangler_Output_Batch_Bake(BakeWrangler_Tree_Node, Node):
|
|
'''Output controller oven node'''
|
|
bl_label = 'Batch Bake'
|
|
|
|
vert_files = []
|
|
|
|
# Makes sure there is always one empty input socket at the bottom by adding and removing sockets
|
|
def update_inputs(self):
|
|
BakeWrangler_Tree_Node.update_inputs(self, 'BakeWrangler_Socket_Bake', "Bake")
|
|
|
|
# Check node settings are valid to bake. Returns true/false, plus error message(s).
|
|
def validate(self, is_primary=True):
|
|
valid = [True]
|
|
# Batch mode needs to avoid validating the same things more than once. Collect a
|
|
# unique list of the passes before validating them.
|
|
img_node_list = []
|
|
pass_node_list = []
|
|
for input in self.inputs:
|
|
if input.islinked() and input.valid:
|
|
img_node = follow_input_link(input.links[0]).from_node
|
|
if not img_node_list.count(img_node):
|
|
img_node_list.append(img_node)
|
|
for img_node_input in img_node.inputs:
|
|
if img_node_input.islinked() and img_node_input.valid:
|
|
pass_node = follow_input_link(img_node_input.links[0]).from_node
|
|
if not pass_node_list.count(pass_node):
|
|
pass_node_list.append(pass_node)
|
|
# Validate all the listed nodes
|
|
has_valid_input = False
|
|
for node in img_node_list:
|
|
img_node_valid = node.validate()
|
|
if not img_node_valid.pop(0):
|
|
valid[0] = False
|
|
if valid[0]:
|
|
has_valid_input = True
|
|
valid += img_node_valid
|
|
for node in pass_node_list:
|
|
pass_node_valid = node.validate()
|
|
if not pass_node_valid.pop(0):
|
|
valid[0] = False
|
|
valid += pass_node_valid
|
|
errs = len(valid)
|
|
if not has_valid_input and errs < 2:
|
|
valid[0] = False
|
|
valid.append([_print("Input error", node=self, ret=True), ": No valid inputs connected"])
|
|
# Validate pre and post scripts if set to external file
|
|
if self.loc_pre == 'EXT':
|
|
file_path = self.script_pre_can
|
|
# Validate file path
|
|
if os.path.exists(file_path):
|
|
if os.path.isfile(file_path):
|
|
# It exists so try to open it for read
|
|
try:
|
|
file = open(file_path, "r")
|
|
except OSError as err:
|
|
valid[0] = False
|
|
valid.append([_print("Pre-Script file error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)])
|
|
else:
|
|
# It exists but isn't a file
|
|
valid[0] = False
|
|
valid.append([_print("Pre-Script file error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)])
|
|
# See if the pre script compiles if set
|
|
if self.loc_pre != 'NON':
|
|
if self.loc_pre == 'INT':
|
|
pre_scr = self.script_pre_int.as_string()
|
|
elif self.loc_pre == 'EXT':
|
|
with open(self.script_pre_can, "r") as scr:
|
|
pre_scr = scr.read()
|
|
try:
|
|
compile(pre_scr, '<string>', 'exec')
|
|
except SyntaxError or ValueError as err:
|
|
valid[0] = False
|
|
valid.append([_print("Pre-Script compile error", node=self, ret=True), ": %s" % (str(err))])
|
|
if self.loc_post == 'EXT':
|
|
file_path = self.script_post_can
|
|
# Validate file path
|
|
if os.path.exists(file_path):
|
|
if os.path.isfile(file_path):
|
|
# It exists so try to open it for read
|
|
try:
|
|
file = open(file_path, "r")
|
|
except OSError as err:
|
|
valid[0] = False
|
|
valid.append([_print("Post-Script file error", node=self, ret=True), ": Trying to open file at '%s'" % (err.strerror)])
|
|
else:
|
|
# It exists but isn't a file
|
|
valid[0] = False
|
|
valid.append([_print("Post-Script file error", node=self, ret=True), ": File exists but isn't a regular file '%s'" % (file_path)])
|
|
# See if the pre script compiles if set
|
|
if self.loc_post != 'NON':
|
|
if self.loc_post == 'INT':
|
|
post_scr = self.script_post_int.as_string()
|
|
elif self.loc_post == 'EXT':
|
|
with open(self.script_post_can, "r") as scr:
|
|
post_scr = scr.read()
|
|
try:
|
|
compile(post_scr, '<string>', 'exec')
|
|
except SyntaxError or ValueError as err:
|
|
valid[0] = False
|
|
valid.append([_print("Post-Script compile error", node=self, ret=True), ": %s" % (str(err))])
|
|
# Everything validated
|
|
return valid
|
|
|
|
# Get a list of unique objects used as either source or target
|
|
def get_unique_objects(self, type):
|
|
objs = []
|
|
for input in self.inputs:
|
|
input = get_input(input)
|
|
if input and input.bl_idname == 'BakeWrangler_Output_Image_Path':
|
|
objs += input.get_unique_objects(type)
|
|
objs = prune_objects(objs)
|
|
objs_single = []
|
|
for obj in objs:
|
|
objs_single.append(obj[0])
|
|
return objs_single
|
|
|
|
# Generate enum of base collection types
|
|
def get_collection_types(self, context):
|
|
col_types = []
|
|
for prop in bpy.data.bl_rna.properties:
|
|
if prop.type == 'COLLECTION':
|
|
col_types.append((prop.identifier, prop.name, prop.description))
|
|
return col_types
|
|
|
|
# Generate enum of props
|
|
def get_user_props(self, context):
|
|
props = []
|
|
if self.user_prop_objt:
|
|
for prop in self.user_prop_objt.keys():
|
|
props.append((prop, prop, prop + " custom property"))
|
|
return props
|
|
|
|
# Set the nodes background color to red when shutdown is enabled
|
|
def shutdown_update(self, context):
|
|
if self.shutdown_after:
|
|
self.use_custom_color = True
|
|
self.color = [0.9, 0, 0]
|
|
else:
|
|
self.use_custom_color = False
|
|
self.color = [0.608, 0.608, 0.608]
|
|
|
|
# Get full path, removing any relative references
|
|
def update_pre_script(self, context):
|
|
cwd = os.path.dirname(bpy.data.filepath)
|
|
self.script_pre_can = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.script_pre)))
|
|
|
|
# Get full path, removing any relative references
|
|
def update_post_script(self, context):
|
|
cwd = os.path.dirname(bpy.data.filepath)
|
|
self.script_post_can = os.path.normpath(os.path.join(cwd, bpy.path.abspath(self.script_post)))
|
|
|
|
loc = (
|
|
('EXT', "External", ""),
|
|
('INT', "Internal", ""),
|
|
('NON', "None", ""),
|
|
)
|
|
|
|
loc_pre: bpy.props.EnumProperty(name="Script Location", description="Either internal or external file", items=loc, default='NON')
|
|
script_pre: bpy.props.StringProperty(name="Script path", description="Path to python script to run", default="", subtype='FILE_PATH', update=update_pre_script)
|
|
script_pre_can: bpy.props.StringProperty(name="Canical Script File", description="Canicial path to script", default="", subtype='FILE_PATH')
|
|
script_pre_int: bpy.props.PointerProperty(name="Script", description="Python script to run", type=bpy.types.Text)
|
|
|
|
loc_post: bpy.props.EnumProperty(name="Script Location", description="Either internal or external file", items=loc, default='NON')
|
|
script_post: bpy.props.StringProperty(name="Script path", description="Path to python script to run", default="", subtype='FILE_PATH', update=update_post_script)
|
|
script_post_can: bpy.props.StringProperty(name="Canical Script File", description="Canicial path to script", default="", subtype='FILE_PATH')
|
|
script_post_int: bpy.props.PointerProperty(name="Script", description="Python script to run", type=bpy.types.Text)
|
|
|
|
user_prop: bpy.props.BoolProperty(name="User Property", description="Enable custom user property incrementer", default=False)
|
|
user_prop_type: bpy.props.EnumProperty(name="Type", description="Type of data property is on", items=get_collection_types)
|
|
user_prop_objt: bpy.props.PointerProperty(name="Data", description="Data property is on", type=bpy.types.ID)
|
|
user_prop_name: bpy.props.EnumProperty(name="Name", description="Name of the property", items=get_user_props)
|
|
user_prop_zero: bpy.props.BoolProperty(name="Zero on Bake", description="Resets property to zero when bake starts", default=True)
|
|
shutdown_after: bpy.props.BoolProperty(name="Shutdown on Completion", description="Attempt to shutdown system after batch bake completes", default=False, update=shutdown_update)
|
|
|
|
def init(self, context):
|
|
BakeWrangler_Tree_Node.init(self, context)
|
|
self.inputs.new('BakeWrangler_Socket_Bake', "Bake")
|
|
|
|
def draw_buttons(self, context, layout):
|
|
BakeWrangler_Tree_Node.draw_bake_button(self, layout, 'OUTLINER', "Bake All")
|
|
row = layout.row(align=True)
|
|
if not len(self.vert_files):
|
|
row.operator("bake_wrangler.dummy_vcol", icon='CHECKMARK', text="Apply")
|
|
row.operator("bake_wrangler.dummy_vcol", icon='PANEL_CLOSE', text="Discard")
|
|
else:
|
|
op1 = row.operator("bake_wrangler.apply_vertcols", icon='CHECKMARK', text="Apply")
|
|
op1.tree = self.id_data.name
|
|
op1.node = self.name
|
|
op2 = row.operator("bake_wrangler.discard_vertcols", icon='PANEL_CLOSE', text="Discard")
|
|
op2.tree = self.id_data.name
|
|
op2.node = self.name
|
|
layout.label(text="Bake Images:")
|
|
|
|
def draw_buttons_ext(self, context, layout):
|
|
col = layout.column(align=False)
|
|
|
|
col.label(text="Pre-Bake Script:")
|
|
row = col.row(align=True)
|
|
row.prop_enum(self, "loc_pre", "NON")
|
|
row.prop_enum(self, "loc_pre", "INT")
|
|
row.prop_enum(self, "loc_pre", "EXT")
|
|
if self.loc_pre == 'EXT':
|
|
col.prop(self, "script_pre", text="")
|
|
elif self.loc_pre == 'INT':
|
|
col.prop_search(self, "script_pre_int", bpy.data, "texts", text="")
|
|
|
|
col.label(text="Post-Bake Script:")
|
|
row = col.row(align=True)
|
|
row.prop_enum(self, "loc_post", "NON")
|
|
row.prop_enum(self, "loc_post", "INT")
|
|
row.prop_enum(self, "loc_post", "EXT")
|
|
if self.loc_post == 'EXT':
|
|
col.prop(self, "script_post", text="")
|
|
elif self.loc_post == 'INT':
|
|
col.prop_search(self, "script_post_int", bpy.data, "texts", text="")
|
|
|
|
row = col.row(align=True)
|
|
row.prop(self, "user_prop", text="Increment Property")
|
|
if self.user_prop:
|
|
col.prop(self, "user_prop_type")
|
|
col.prop_search(self, "user_prop_objt", bpy.data, self.user_prop_type)
|
|
col.prop(self, "user_prop_name")
|
|
col.prop(self, "user_prop_zero")
|
|
col.prop(self, "shutdown_after")
|
|
|
|
|
|
|
|
#
|
|
# Node Categories
|
|
#
|
|
|
|
import nodeitems_utils
|
|
from nodeitems_utils import NodeCategory, NodeItem
|
|
|
|
# Base class for the node category menu system
|
|
class BakeWrangler_Node_Category(NodeCategory):
|
|
@classmethod
|
|
def poll(cls, context):
|
|
tree_type = getattr(context.space_data, "tree_type", None)
|
|
return tree_type == 'BakeWrangler_Tree'
|
|
|
|
# List of all bakery nodes put into categories with identifier, name
|
|
BakeWrangler_Node_Categories = [
|
|
BakeWrangler_Node_Category('BakeWrangler_Settings', "Settings", items=[
|
|
NodeItem("BakeWrangler_MeshSettings"),
|
|
NodeItem("BakeWrangler_SampleSettings"),
|
|
NodeItem("BakeWrangler_PassSettings"),
|
|
NodeItem("BakeWrangler_OutputSettings"),
|
|
]),
|
|
BakeWrangler_Node_Category('BakeWrangler_Nodes', "Bake", items=[
|
|
NodeItem("BakeWrangler_Input_Filenames"),
|
|
NodeItem("BakeWrangler_Input_ObjectList"),
|
|
NodeItem("BakeWrangler_Bake_Billboard"),
|
|
NodeItem("BakeWrangler_Bake_Material"),
|
|
NodeItem("BakeWrangler_Bake_Mesh"),
|
|
NodeItem("BakeWrangler_Sort_Meshes"),
|
|
NodeItem("BakeWrangler_Bake_Pass"),
|
|
NodeItem("BakeWrangler_Output_Image_Path"),
|
|
NodeItem("BakeWrangler_Output_Vertex_Cols"),
|
|
NodeItem("BakeWrangler_Output_Batch_Bake"),
|
|
]),
|
|
BakeWrangler_Node_Category('BakeWrangler_Post', "Post", items=[
|
|
NodeItem("BakeWrangler_Channel_Map"),
|
|
NodeItem("BakeWrangler_Post_MixRGB"),
|
|
NodeItem("BakeWrangler_Post_SplitRGB"),
|
|
NodeItem("BakeWrangler_Post_JoinRGB"),
|
|
NodeItem("BakeWrangler_Post_Math"),
|
|
NodeItem("BakeWrangler_Post_Gamma"),
|
|
]),
|
|
BakeWrangler_Node_Category('BakeWrangler_Layout', "Layout", items=[
|
|
NodeItem("NodeFrame"),
|
|
NodeItem("NodeReroute"),
|
|
]),
|
|
]
|
|
|
|
|
|
#
|
|
# Registration
|
|
#
|
|
|
|
# All bakery classes that need to be registered
|
|
classes = (
|
|
BakeWrangler_Operator_Dummy,
|
|
BakeWrangler_Operator_Dummy_VCol,
|
|
BakeWrangler_Operator_FilterToggle,
|
|
BakeWrangler_Operator_DoubleVal,
|
|
BakeWrangler_Operator_PickMenuEnum,
|
|
BakeWrangler_Operator_AddSelected,
|
|
BakeWrangler_Operator_DiscardBakedVertCols,
|
|
BakeWrangler_Operator_ApplyBakedVertCols,
|
|
BakeWrangler_Operator_BakeStop,
|
|
BakeWrangler_Operator_BakePass,
|
|
BakeWrangler_Tree,
|
|
BakeWrangler_Socket_Object,
|
|
BakeWrangler_Socket_Material,
|
|
BakeWrangler_Socket_Mesh,
|
|
BakeWrangler_Socket_Color,
|
|
BakeWrangler_Socket_ChanMap,
|
|
BakeWrangler_Socket_Float,
|
|
BakeWrangler_Socket_Bake,
|
|
BakeWrangler_Socket_MeshSetting,
|
|
BakeWrangler_Socket_PassSetting,
|
|
BakeWrangler_Socket_SampleSetting,
|
|
BakeWrangler_Socket_OutputSetting,
|
|
BakeWrangler_Socket_ObjectNames,
|
|
BakeWrangler_Socket_SplitOutput,
|
|
BakeWrangler_MeshSettings,
|
|
BakeWrangler_SampleSettings,
|
|
BakeWrangler_PassSettings,
|
|
BakeWrangler_OutputSettings,
|
|
BakeWrangler_Input_ObjectList,
|
|
BakeWrangler_Input_Filenames,
|
|
BakeWrangler_Bake_Billboard,
|
|
BakeWrangler_Bake_Material,
|
|
BakeWrangler_Bake_Mesh,
|
|
BakeWrangler_Sort_Meshes,
|
|
BakeWrangler_Bake_Pass,
|
|
BakeWrangler_Channel_Map,
|
|
BakeWrangler_Post_MixRGB,
|
|
BakeWrangler_Post_SplitRGB,
|
|
BakeWrangler_Post_JoinRGB,
|
|
BakeWrangler_Post_Math,
|
|
BakeWrangler_Post_Math_OpMenu,
|
|
BakeWrangler_Post_Gamma,
|
|
BakeWrangler_Output_Image_Path,
|
|
BakeWrangler_Output_Vertex_Cols,
|
|
BakeWrangler_Output_Batch_Bake,
|
|
)
|
|
|
|
|
|
def register():
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|
|
nodeitems_utils.register_node_categories('BakeWrangler_Nodes', BakeWrangler_Node_Categories)
|
|
|
|
|
|
def unregister():
|
|
nodeitems_utils.unregister_node_categories('BakeWrangler_Nodes')
|
|
from bpy.utils import unregister_class
|
|
for cls in reversed(classes):
|
|
unregister_class(cls)
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register()
|