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" % (output, "PWRAP") else: output = "%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 == "": 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 == "": tag_end = True out += '\n' elif tag_line == "": tag_end = True tag_out += '\n' #sys.stdout.write('\n') #sys.stdout.flush() elif tag_line == "": tag_line += sub.stdout.read(8) tag_end = True self._success = True self._finish = True elif tag_line == "": 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 == "" or out == "": tag_end = False tag_line = "" files = "" # Set output queue if out == "": 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 == "" or tag_line == "": tag_end = True _print(files, enque=que, wrap=False) if tag != '' and not tag_end: files += tag out = '' if out == "" or out == "" or out == "": 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 == "": tag_end = True frames_itr = int(num) elif tag_line == "": tag_end = True solution_itr = int(num) elif tag_line == "": 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, '', '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, '', '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()