# SPDX-License-Identifier: GPL-2.0-or-later # TO DO LIST # # Add more options to curve radius/modulation plus cyclic/connect curve option import bpy # import selection_utils from bpy.types import Operator from random import ( choice as rand_choice, random as rand_random, randint as rand_randint, uniform as rand_uniform, ) # Selection Module: Contributors: Mackraken, Andrew Hale (TrumanBlending) # Adapted from Mackraken's "Tools for Curves" addon selected = [] class SelectionOrder(bpy.types.Operator): """Store the object names in the order they are selected, """ \ """use RETURN key to confirm selection, ESCAPE key to cancel""" bl_idname = "object.select_order" bl_label = "Select with Order" bl_options = {'UNDO'} num_selected = 0 @classmethod def poll(self, context): return bpy.context.mode == 'OBJECT' def update(self, context): # Get the currently selected objects sel = context.selected_objects num = len(sel) if num == 0: # Reset the list del selected[:] elif num > self.num_selected: # Get all the newly selected objects and add new = [ob.name for ob in sel if ob.name not in selected] selected.extend(new) elif num < self.num_selected: # Get the selected objects and remove from list curnames = {ob.name for ob in sel} selected[:] = [name for name in selected if name in curnames] # Set the number of currently select objects self.num_selected = len(selected) def modal(self, context, event): if event.type == 'RET': # If return is pressed, finish the operator return {'FINISHED'} elif event.type == 'ESC': # If escape is pressed, cancel the operator return {'CANCELLED'} # Update selection if we need to self.update(context) return {'PASS_THROUGH'} def invoke(self, context, event): self.update(context) context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def error_handlers(self, op_name, error, reports="ERROR", func=False): if self and reports: self.report({'WARNING'}, reports + " (See Console for more info)") is_func = "Function" if func else "Operator" print("\n[Btrace]\n{}: {}\nError: {}\n".format(op_name, is_func, error)) # Object Trace # creates a curve with a modulated radius connecting points of a mesh class OBJECT_OT_objecttrace(Operator): bl_idname = "object.btobjecttrace" bl_label = "Btrace: Object Trace" bl_description = ("Trace selected mesh object with a curve with the option to animate\n" "The Active Object has to be of a Mesh or Font type") bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.object and context.object.type in {'MESH', 'FONT'}) def invoke(self, context, event): try: # Run through each selected object and convert to to a curved object brushObj = context.selected_objects Btrace = context.window_manager.curve_tracer check_materials = True # Duplicate Mesh if Btrace.object_duplicate: bpy.ops.object.duplicate_move() brushObj = context.selected_objects # Join Mesh if Btrace.convert_joinbefore: if len(brushObj) > 1: # Only run if multiple objects selected bpy.ops.object.join() brushObj = context.selected_objects for i in brushObj: context.view_layer.objects.active = i if i and i.type != 'CURVE': bpy.ops.object.btconvertcurve() # Materials trace_mats = addtracemat(bpy.context.object.data) if not trace_mats and check_materials is True: check_materials = False if Btrace.animate: bpy.ops.curve.btgrow() if check_materials is False: self.report({'WARNING'}, "Some Materials could not be added") return {'FINISHED'} except Exception as e: error_handlers(self, "object.btobjecttrace", e, "Object Trace could not be completed") return {'CANCELLED'} # Objects Connect # connect selected objects with a curve + hooks to each node # possible handle types: 'FREE' 'AUTO' 'VECTOR' 'ALIGNED' class OBJECT_OT_objectconnect(Operator): bl_idname = "object.btobjectsconnect" bl_label = "Btrace: Objects Connect" bl_description = ("Connect selected objects with a curve and add hooks to each node\n" "Needs at least two objects selected") bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return len(context.selected_objects) > 1 def invoke(self, context, event): try: lists = [] Btrace = context.window_manager.curve_tracer curve_handle = Btrace.curve_handle if curve_handle == 'AUTOMATIC': # hackish because of naming conflict in api curve_handle = 'AUTO' # Check if Btrace group exists, if not create bcollection = bpy.data.collections.keys() if 'Btrace' not in bcollection: mycol=bpy.data.collections.new(name="Btrace") bpy.context.scene.collection.children.link(mycol) # check if noise if Btrace.connect_noise: bpy.ops.object.btfcnoise() # check if respect order is checked, create list of objects if Btrace.respect_order is True: selobnames = selection_utils.selected obnames = [] for ob in selobnames: obnames.append(bpy.data.objects[ob]) else: obnames = bpy.context.selected_objects # No selection order for a in obnames: lists.append(a) a.select_set(False) # trace the origins tracer = bpy.data.curves.new('tracer', 'CURVE') tracer.dimensions = '3D' spline = tracer.splines.new('BEZIER') spline.bezier_points.add(len(lists) - 1) curve = bpy.data.objects.new('curve', tracer) bpy.context.collection.objects.link(curve) # render ready curve tracer.resolution_u = Btrace.curve_u # Set bevel resolution from Panel options tracer.bevel_resolution = Btrace.curve_resolution tracer.fill_mode = 'FULL' # Set bevel depth from Panel options tracer.bevel_depth = Btrace.curve_depth # move nodes to objects for i in range(len(lists)): p = spline.bezier_points[i] p.co = lists[i].location p.handle_right_type = curve_handle p.handle_left_type = curve_handle bpy.context.view_layer.objects.active = curve bpy.ops.object.mode_set(mode='OBJECT') # place hooks for i in range(len(lists)): lists[i].select_set(True) curve.data.splines[0].bezier_points[i].select_control_point = True bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.hook_add_selob() bpy.ops.object.mode_set(mode='OBJECT') curve.data.splines[0].bezier_points[i].select_control_point = False lists[i].select_set(False) bpy.ops.object.select_all(action='DESELECT') curve.select_set(True) # selected curve after it's created # Materials check_materials = True trace_mats = addtracemat(bpy.context.object.data) if not trace_mats and check_materials is True: check_materials = False if Btrace.animate: # Add Curve Grow it? bpy.ops.curve.btgrow() bpy.data.collections["Btrace"].objects.link(curve) # add to Btrace collection # Check if we add grow curve if Btrace.animate: bpy.ops.curve.btgrow() # Add grow curve if check_materials is False: self.report({'WARNING'}, "Some Materials could not be added") return {'FINISHED'} except Exception as e: error_handlers(self, "object.btobjectsconnect", e, "Objects Connect could not be completed") return {'CANCELLED'} # Particle Trace # creates a curve from each particle of a system def curvetracer(curvename, splinename): Btrace = bpy.context.window_manager.curve_tracer tracer = bpy.data.curves.new(splinename, 'CURVE') tracer.dimensions = '3D' curve = bpy.data.objects.new(curvename, tracer) bpy.context.collection.objects.link(curve) try: tracer.fill_mode = 'FULL' except: tracer.use_fill_front = tracer.use_fill_back = False tracer.bevel_resolution = Btrace.curve_resolution tracer.bevel_depth = Btrace.curve_depth tracer.resolution_u = Btrace.curve_u return tracer, curve # Particle Trace class OBJECT_OT_particletrace(Operator): bl_idname = "particles.particletrace" bl_label = "Btrace: Particle Trace" bl_description = ("Creates a curve from each particle of a system.\n" "Keeping particle amount under 250 will make this run faster") bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.object is not None and context.object.particle_systems) def execute(self, context): try: Btrace = bpy.context.window_manager.curve_tracer particle_step = Btrace.particle_step # step size in frames obj = bpy.context.object obj = bpy.context.evaluated_depsgraph_get().objects.get(obj.name, None) ps = obj.particle_systems.active curvelist = [] curve_handle = Btrace.curve_handle check_materials = True if curve_handle == 'AUTOMATIC': # hackish naming conflict curve_handle = 'AUTO' if curve_handle == 'FREE_ALIGN': # hackish naming conflict curve_handle = 'FREE' # Check if Btrace group exists, if not create bcollection = bpy.data.collections.keys() if 'Btrace' not in bcollection: mycol=bpy.data.collections.new(name="Btrace") bpy.context.scene.collection.children.link(mycol) if Btrace.curve_join: tracer = curvetracer('Tracer', 'Splines') for x in ps.particles: if not Btrace.curve_join: tracer = curvetracer('Tracer.000', 'Spline.000') spline = tracer[0].splines.new('BEZIER') # add point to spline based on step size spline.bezier_points.add((x.lifetime - 1) // particle_step) for t in list(range(int(x.lifetime))): bpy.context.scene.frame_set(t + x.birth_time) if not t % particle_step: p = spline.bezier_points[t // particle_step] p.co = x.location p.handle_right_type = curve_handle p.handle_left_type = curve_handle particlecurve = tracer[1] curvelist.append(particlecurve) # add to group bpy.ops.object.select_all(action='DESELECT') for curveobject in curvelist: curveobject.select_set(True) bpy.context.view_layer.objects.active = curveobject bpy.data.collections["Btrace"].objects.link(curveobject) # Materials trace_mats = addtracemat(curveobject.data) if not trace_mats and check_materials is True: check_materials = False if Btrace.animate: bpy.ops.curve.btgrow() # Add grow curve if check_materials is False: self.report({'WARNING'}, "Some Materials could not be added") return {'FINISHED'} except Exception as e: error_handlers(self, "particles.particletrace", e, "Particle Trace could not be completed") return {'CANCELLED'} # Particle Connect # connect all particles in active system with a continuous animated curve class OBJECT_OT_traceallparticles(Operator): bl_idname = "particles.connect" bl_label = "Connect Particles" bl_description = ("Create a continuous animated curve from particles in active system\n" "Needs an Object with a particle system attached") bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.object is not None and context.object.particle_systems) def execute(self, context): try: obj = context.object obj = bpy.context.evaluated_depsgraph_get().objects.get(obj.name, None) ps = obj.particle_systems.active setting = ps.settings # Grids distribution not supported if setting.distribution == 'GRID': self.report({'INFO'}, "Grid distribution mode for particles not supported") return{'CANCELLED'} Btrace = bpy.context.window_manager.curve_tracer # Get frame start particle_f_start = Btrace.particle_f_start # Get frame end particle_f_end = Btrace.particle_f_end curve_handle = Btrace.curve_handle # hackish because of naming conflict in api if curve_handle == 'AUTOMATIC': curve_handle = 'AUTO' if curve_handle == 'FREE_ALIGN': curve_handle = 'FREE' # define what kind of object to create tracer = bpy.data.curves.new('Splines', 'CURVE') # Create new object with settings listed above curve = bpy.data.objects.new('Tracer', tracer) # Link newly created object to the scene # bpy.context.view_layer.objects.link(curve) bpy.context.scene.collection.objects.link(curve) # add a new Bezier point in the new curve spline = tracer.splines.new('BEZIER') spline.bezier_points.add(setting.count - 1) tracer.dimensions = '3D' tracer.resolution_u = Btrace.curve_u tracer.bevel_resolution = Btrace.curve_resolution tracer.fill_mode = 'FULL' tracer.bevel_depth = Btrace.curve_depth if Btrace.particle_auto: f_start = int(setting.frame_start) f_end = int(setting.frame_end + setting.lifetime) else: if particle_f_end <= particle_f_start: particle_f_end = particle_f_start + 1 f_start = particle_f_start f_end = particle_f_end for bFrames in range(f_start, f_end): bpy.context.scene.frame_set(bFrames) if not (bFrames - f_start) % Btrace.particle_step: for bFrames in range(setting.count): if ps.particles[bFrames].alive_state != 'UNBORN': e = bFrames bp = spline.bezier_points[bFrames] pt = ps.particles[e] bp.co = pt.location bp.handle_right_type = curve_handle bp.handle_left_type = curve_handle bp.keyframe_insert('co') bp.keyframe_insert('handle_left') bp.keyframe_insert('handle_right') # Select new curve bpy.ops.object.select_all(action='DESELECT') curve.select_set(True) bpy.context.view_layer.objects.active = curve # Materials trace_mats = addtracemat(curve.data) if not trace_mats: self.report({'WARNING'}, "Some Materials could not be added") if Btrace.animate: bpy.ops.curve.btgrow() return{'FINISHED'} except Exception as e: error_handlers(self, "particles.connect", e, "Connect Particles could not be completed") return {'CANCELLED'} # Writing Tool # Writes a curve by animating its point's radii class OBJECT_OT_writing(Operator): bl_idname = "curve.btwriting" bl_label = "Write" bl_description = ("Use Grease Pencil to write and convert to curves\n" "Needs an existing Grease Pencil layer attached to the Scene") bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): gp = context.scene.grease_pencil return gp and gp.layers def execute(self, context): try: # first check if the Grease Pencil is attached to the Scene tool_settings = context.scene.tool_settings source_data = tool_settings.grease_pencil_source if source_data in {"OBJECT"}: self.report({'WARNING'}, "Operation Cancelled. " "The Grease Pencil data-block is attached to an Object") return {"CANCELLED"} Btrace = context.window_manager.curve_tracer # this is hacky - store objects in the scene for comparison later store_objects = [ob for ob in context.scene.objects] gactive = context.active_object # checking if there are any strokes the easy way if not bpy.ops.gpencil.convert.poll(): self.report({'WARNING'}, "Operation Cancelled. " "Are there any Grease Pencil Strokes ?") return {'CANCELLED'} bpy.ops.gpencil.convert(type='CURVE') # get curve after convert (compare the scenes to get the difference) scene_obj = context.scene.objects check_materials = True for obj in scene_obj: if obj not in store_objects and obj.type == "CURVE": gactiveCurve = obj break # render ready curve gactiveCurve.data.resolution_u = Btrace.curve_u # Set bevel resolution from Panel options gactiveCurve.data.bevel_resolution = Btrace.curve_resolution gactiveCurve.data.fill_mode = 'FULL' # Set bevel depth from Panel options gactiveCurve.data.bevel_depth = Btrace.curve_depth writeObj = context.selected_objects if Btrace.animate: for i in writeObj: context.view_layer.objects.active = i bpy.ops.curve.btgrow() # Materials trace_mats = addtracemat(bpy.context.object.data) if not trace_mats and check_materials is True: check_materials = False else: for i in writeObj: context.view_layer.objects.active = i # Materials trace_mats = addtracemat(bpy.context.object.data) if not trace_mats and check_materials is True: check_materials = False # Delete grease pencil strokes context.view_layer.objects.active = gactive bpy.ops.gpencil.data_unlink() context.view_layer.objects.active = gactiveCurve # Smooth object bpy.ops.object.shade_smooth() # Return to first frame bpy.context.scene.frame_set(Btrace.anim_f_start) if check_materials is False: self.report({'WARNING'}, "Some Materials could not be added") return{'FINISHED'} except Exception as e: error_handlers(self, "curve.btwriting", e, "Grease Pencil conversion could not be completed") return {'CANCELLED'} # Create Curve # Convert mesh to curve using either Continuous, All Edges, or Sharp Edges # Option to create noise class OBJECT_OT_convertcurve(Operator): bl_idname = "object.btconvertcurve" bl_label = "Btrace: Create Curve" bl_description = ("Convert Mesh to Curve using either Continuous, " "All Edges or Sharp Edges\n" "Active Object has to be of a Mesh or Font type") bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.object is not None and context.object.type in {"MESH", "FONT"}) def execute(self, context): try: Btrace = context.window_manager.curve_tracer obj = context.object # Convert Font if obj.type == 'FONT': bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.convert(target='CURVE') # Convert edges to curve bpy.context.object.data.dimensions = '3D' # make a continuous edge through all vertices if obj.type == 'MESH': # Add noise to mesh if Btrace.distort_curve: for v in obj.data.vertices: for u in range(3): v.co[u] += Btrace.distort_noise * (rand_uniform(-1, 1)) if Btrace.convert_edgetype == 'CONTI': # Start Continuous edge bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.delete(type='EDGE_FACE') bpy.ops.mesh.select_all(action='DESELECT') verts = bpy.context.object.data.vertices bpy.ops.object.mode_set(mode='OBJECT') li = [] p1 = rand_randint(0, len(verts) - 1) for v in verts: li.append(v.index) li.remove(p1) for z in range(len(li)): x = [] for px in li: d = verts[p1].co - verts[px].co # find distance from first vert x.append(d.length) p2 = li[x.index(min(x))] # find the shortest distance list index verts[p1].select = verts[p2].select = True bpy.ops.object.mode_set(mode='EDIT') bpy.context.tool_settings.mesh_select_mode = [True, False, False] bpy.ops.mesh.edge_face_add() bpy.ops.mesh.select_all(action='DESELECT') bpy.ops.object.mode_set(mode='OBJECT') li.remove(p2) # remove item from list. p1 = p2 # Convert edges to curve bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.convert(target='CURVE') if Btrace.convert_edgetype == 'EDGEALL': # Start All edges bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.delete(type='ONLY_FACE') bpy.ops.object.mode_set() bpy.ops.object.convert(target='CURVE') for sp in obj.data.splines: sp.type = Btrace.curve_spline obj = context.object # Set spline type to custom property in panel bpy.ops.object.editmode_toggle() bpy.ops.curve.spline_type_set(type=Btrace.curve_spline) # Set handle type to custom property in panel bpy.ops.curve.handle_type_set(type=Btrace.curve_handle) bpy.ops.object.editmode_toggle() obj.data.fill_mode = 'FULL' # Set resolution to custom property in panel obj.data.bevel_resolution = Btrace.curve_resolution obj.data.resolution_u = Btrace.curve_u # Set depth to custom property in panel obj.data.bevel_depth = Btrace.curve_depth # Smooth object bpy.ops.object.shade_smooth() # Modulate curve radius and add distortion if Btrace.distort_curve: scale = Btrace.distort_modscale if scale == 0: return {'FINISHED'} for u in obj.data.splines: for v in u.bezier_points: v.radius = scale * round(rand_random(), 3) return {'FINISHED'} except Exception as e: error_handlers(self, "object.btconvertcurve", e, "Conversion could not be completed") return {'CANCELLED'} # Mesh Follow, trace vertex or faces # Create curve at center of selection item, extruded along animation # Needs to be an animated mesh!!! class OBJECT_OT_meshfollow(Operator): bl_idname = "object.btmeshfollow" bl_label = "Btrace: Vertex Trace" bl_description = "Trace Vertex or Face on an animated mesh" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.object and context.object.type in {'MESH'}) def execute(self, context): try: Btrace = context.window_manager.curve_tracer stepsize = Btrace.particle_step obj = context.object scn = context.scene drawmethod = Btrace.fol_mesh_type # Draw from Edges, Verts, or Faces if drawmethod == 'VERTS': meshobjs = obj.data.vertices if drawmethod == 'FACES': meshobjs = obj.data.polygons # untested if drawmethod == 'EDGES': meshobjs = obj.data.edges # untested # Frame properties start_frame, end_frame = Btrace.fol_start_frame, Btrace.fol_end_frame if start_frame > end_frame: # Make sure the math works start_frame = end_frame - 5 # if start past end, goto (end - 5) frames = int((end_frame - start_frame) / stepsize) def getsel_option(): # Get selection objects sel = [] # options are 'random', 'custom', 'all' seloption, fol_mesh_type = Btrace.fol_sel_option, Btrace.fol_mesh_type if fol_mesh_type == 'OBJECT': pass else: if seloption == 'CUSTOM': for i in meshobjs: if i.select_get() is True: sel.append(i.index) if seloption == 'RANDOM': for i in list(meshobjs): sel.append(i.index) finalsel = int(len(sel) * Btrace.fol_perc_verts) remove = len(sel) - finalsel for i in range(remove): sel.pop(rand_randint(0, len(sel) - 1)) if seloption == 'ALL': for i in list(meshobjs): sel.append(i.index) return sel def get_coord(objindex): obj_co = [] # list of vector coordinates to use frame_x = start_frame for i in range(frames): # create frame numbers list scn.frame_set(frame_x) if drawmethod != 'OBJECT': followed_item = meshobjs[objindex] if drawmethod == 'VERTS': # find Vert vector g_co = obj.matrix_local @ followed_item.co if drawmethod == 'FACES': # find Face vector g_co = obj.matrix_local @ followed_item.normal if drawmethod == 'EDGES': v1 = followed_item.vertices[0] v2 = followed_item.vertices[1] co1 = bpy.context.object.data.vertices[v1] co2 = bpy.context.object.data.vertices[v2] localcenter = co1.co.lerp(co2.co, 0.5) g_co = obj.matrix_world @ localcenter if drawmethod == 'OBJECT': g_co = objindex.location.copy() obj_co.append(g_co) frame_x = frame_x + stepsize scn.frame_set(start_frame) return obj_co def make_curve(co_list): Btrace = bpy.context.window_manager.curve_tracer tracer = bpy.data.curves.new('tracer', 'CURVE') tracer.dimensions = '3D' spline = tracer.splines.new('BEZIER') spline.bezier_points.add(len(co_list) - 1) curve = bpy.data.objects.new('curve', tracer) scn.collection.objects.link(curve) curvelist.append(curve) # render ready curve tracer.resolution_u = Btrace.curve_u # Set bevel resolution from Panel options tracer.bevel_resolution = Btrace.curve_resolution tracer.fill_mode = 'FULL' # Set bevel depth from Panel options tracer.bevel_depth = Btrace.curve_depth curve_handle = Btrace.curve_handle # hackish AUTOMATIC doesn't work here if curve_handle == 'AUTOMATIC': curve_handle = 'AUTO' # move bezier points to objects for i in range(len(co_list)): p = spline.bezier_points[i] p.co = co_list[i] p.handle_right_type = curve_handle p.handle_left_type = curve_handle return curve # Run methods # Check if Btrace group exists, if not create bcollection = bpy.data.collections.keys() if 'Btrace' not in bcollection: mycol=bpy.data.collections.new(name="Btrace") bpy.context.scene.collection.children.link(mycol) Btrace = bpy.context.window_manager.curve_tracer sel = getsel_option() # Get selection curvelist = [] # list to use for grow curve check_materials = True if Btrace.fol_mesh_type == 'OBJECT': vector_list = get_coord(obj) curvelist.append(make_curve(vector_list)) else: for i in sel: print(i) vector_list = get_coord(i) make_curve(vector_list) # curvelist.append(make_curve(vector_list)) # append happens in function # Select new curves and add to group bpy.ops.object.select_all(action='DESELECT') for curveobject in curvelist: if curveobject.type == 'CURVE': curveobject.select_set(True) bpy.context.view_layer.objects.active = curveobject bpy.data.collections["Btrace"].objects.link(curveobject) #2.8 link obj to collection bpy.context.scene.collection.objects.unlink(curveobject) # unlink from scene collection # bpy.ops.object.group_link(group="Btrace") # Materials trace_mats = addtracemat(curveobject.data) if not trace_mats and check_materials is True: check_materials = False curveobject.select_set(False) if Btrace.animate: # Add grow curve for curveobject in curvelist: curveobject.select_set(True) bpy.ops.curve.btgrow() for curveobject in curvelist: curveobject.select_set(False) obj.select_set(False) # Deselect original object if check_materials is False: self.report({'WARNING'}, "Some Materials could not be added") return {'FINISHED'} except Exception as e: error_handlers(self, "object.btmeshfollow", e, "Vertex Trace could not be completed") return {'CANCELLED'} # Add Tracer Material def addtracemat(matobj): try: # Check if a material exists, skip if it does matslots = bpy.context.object.data.materials.items() if len(matslots) < 1: # Make sure there is only one material slot Btrace = bpy.context.window_manager.curve_tracer # Check if color blender is to be run if not Btrace.mat_run_color_blender: # Create Random color for each item if Btrace.trace_mat_random: # Use random color from chosen palette, # assign color lists for each palette brightColors = [ Btrace.brightColor1, Btrace.brightColor2, Btrace.brightColor3, Btrace.brightColor4 ] bwColors = [ Btrace.bwColor1, Btrace.bwColor2 ] customColors = [ Btrace.mmColor1, Btrace.mmColor2, Btrace.mmColor3, Btrace.mmColor4, Btrace.mmColor5, Btrace.mmColor6, Btrace.mmColor7, Btrace.mmColor8 ] earthColors = [ Btrace.earthColor1, Btrace.earthColor2, Btrace.earthColor3, Btrace.earthColor4, Btrace.earthColor5 ] greenblueColors = [ Btrace.greenblueColor1, Btrace.greenblueColor2, Btrace.greenblueColor3 ] if Btrace.mmColors == 'BRIGHT': mat_color = brightColors[ rand_randint(0, len(brightColors) - 1) ] if Btrace.mmColors == 'BW': mat_color = bwColors[ rand_randint(0, len(bwColors) - 1) ] if Btrace.mmColors == 'CUSTOM': mat_color = customColors[ rand_randint(0, len(customColors) - 1) ] if Btrace.mmColors == 'EARTH': mat_color = earthColors[ rand_randint(0, len(earthColors) - 1) ] if Btrace.mmColors == 'GREENBLUE': mat_color = greenblueColors[ rand_randint(0, len(greenblueColors) - 1) ] if Btrace.mmColors == 'RANDOM': mat_color = (rand_random(), rand_random(), rand_random()) else: # Choose Single color mat_color = Btrace.trace_mat_color TraceMat = bpy.data.materials.new('TraceMat') TraceMat.use_nodes = True BSDF = TraceMat.node_tree.nodes[1] r, g, b = mat_color[0], mat_color[1], mat_color[2] BSDF.inputs[0].default_value = [r, g, b, 1] # change node color TraceMat.diffuse_color = [r, g, b, 1] # change viewport color # Add material to object matobj.materials.append(bpy.data.materials.get(TraceMat.name)) else: # Run color blender operator bpy.ops.object.colorblender() return True except Exception as e: error_handlers(False, "addtracemat", e, "Function error", True) return False # Add Color Blender Material # This is the magical material changer! class OBJECT_OT_materialChango(Operator): bl_idname = "object.colorblender" bl_label = "Color Blender" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): try: # properties panel Btrace = context.window_manager.curve_tracer colorObjects = context.selected_objects # Set color lists brightColors = [ Btrace.brightColor1, Btrace.brightColor2, Btrace.brightColor3, Btrace.brightColor4 ] bwColors = [Btrace.bwColor1, Btrace.bwColor2] customColors = [ Btrace.mmColor1, Btrace.mmColor2, Btrace.mmColor3, Btrace.mmColor4, Btrace.mmColor5, Btrace.mmColor6, Btrace.mmColor7, Btrace.mmColor8 ] earthColors = [ Btrace.earthColor1, Btrace.earthColor2, Btrace.earthColor3, Btrace.earthColor4, Btrace.earthColor5 ] greenblueColors = [ Btrace.greenblueColor1, Btrace.greenblueColor2, Btrace.greenblueColor3 ] engine = context.scene.render.engine # Go through each selected object and run the operator for i in colorObjects: theObj = i # Check to see if object has materials checkMaterials = len(theObj.data.materials) if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': materialName = "colorblendMaterial" madMat = bpy.data.materials.new(materialName) madMat.use_nodes = True if checkMaterials == 0: theObj.data.materials.append(madMat) else: theObj.material_slots[0].material = madMat else: # This is internal if checkMaterials == 0: # Add a material materialName = "colorblendMaterial" madMat = bpy.data.materials.new(materialName) theObj.data.materials.append(madMat) else: pass # pass since we have what we need # assign the first material of the object to "mat" madMat = theObj.data.materials[0] # Numbers of frames to skip between keyframes skip = Btrace.mmSkip # Random material function def colorblenderRandom(): randomRGB = (rand_random(), rand_random(), rand_random(), 1) if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': Principled_BSDF = madMat.node_tree.nodes[1] mat_color = randomRGB r, g, b = mat_color[0], mat_color[1], mat_color[2] Principled_BSDF.inputs[0].default_value = [r, g, b, 1] madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1 else: madMat.diffuse_color = randomRGB def colorblenderCustom(): if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': Principled_BSDF = madMat.node_tree.nodes[1] mat_color = rand_choice(customColors) r, g, b = mat_color[0], mat_color[1], mat_color[2] Principled_BSDF.inputs[0].default_value = [r, g, b, 1] madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1 else: madMat.diffuse_color = rand_choice(customColors) # Black and white color def colorblenderBW(): if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': Principled_BSDF = madMat.node_tree.nodes[1] mat_color = rand_choice(bwColors) r, g, b = mat_color[0], mat_color[1], mat_color[2] Principled_BSDF.inputs[0].default_value = [r, g, b, 1] madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1 else: madMat.diffuse_color = rand_choice(bwColors) # Bright colors def colorblenderBright(): if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': Principled_BSDF = madMat.node_tree.nodes[1] mat_color = rand_choice(brightColors) r, g, b = mat_color[0], mat_color[1], mat_color[2] Principled_BSDF.inputs[0].default_value = [r, g, b, 1] madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1 else: madMat.diffuse_color = rand_choice(brightColors) # Earth Tones def colorblenderEarth(): if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': Principled_BSDF = madMat.node_tree.nodes[1] mat_color = rand_choice(earthColors) r, g, b = mat_color[0], mat_color[1], mat_color[2] Principled_BSDF.inputs[0].default_value = [r, g, b, 1] madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1 else: madMat.diffuse_color = rand_choice(earthColors) # Green to Blue Tones def colorblenderGreenBlue(): if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': Principled_BSDF = madMat.node_tree.nodes[1] mat_color = rand_choice(greenblueColors) r, g, b = mat_color[0], mat_color[1], mat_color[2] Principled_BSDF.inputs[0].default_value = [r, g, b, 1] madMat.diffuse_color = mat_color[0], mat_color[1], mat_color[2], 1 else: madMat.diffuse_color = rand_choice(greenblueColors) # define frame start/end variables scn = context.scene start = scn.frame_start end = scn.frame_end # Go to each frame in iteration and add material while start <= (end + (skip - 1)): bpy.context.scene.frame_set(frame=start) # Check what colors setting is checked and run the appropriate function if Btrace.mmColors == 'RANDOM': colorblenderRandom() elif Btrace.mmColors == 'CUSTOM': colorblenderCustom() elif Btrace.mmColors == 'BW': colorblenderBW() elif Btrace.mmColors == 'BRIGHT': colorblenderBright() elif Btrace.mmColors == 'EARTH': colorblenderEarth() elif Btrace.mmColors == 'GREENBLUE': colorblenderGreenBlue() else: pass # Add keyframe to material if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': madMat.node_tree.nodes[ 1].inputs[0].keyframe_insert('default_value') # not sure if this is need, it's viewport color only madMat.keyframe_insert('diffuse_color') else: madMat.keyframe_insert('diffuse_color') # Increase frame number start += skip return{'FINISHED'} except Exception as e: error_handlers(self, "object.colorblender", e, "Color Blender could not be completed") return {'CANCELLED'} # This clears the keyframes class OBJECT_OT_clearColorblender(Operator): bl_idname = "object.colorblenderclear" bl_label = "Clear colorblendness" bl_description = "Clear the color keyframes" bl_options = {'REGISTER', 'UNDO'} def invoke(self, context, event): try: colorObjects = context.selected_objects engine = context.scene.render.engine # Go through each selected object and run the operator for i in colorObjects: theObj = i # assign the first material of the object to "mat" matCl = theObj.data.materials[0] # define frame start/end variables scn = context.scene start = scn.frame_start end = scn.frame_end # Remove all keyframes from diffuse_color, super sloppy while start <= (end + 100): context.scene.frame_set(frame=start) try: if engine == 'CYCLES' or engine == 'BLENDER_EEVEE': matCl.node_tree.nodes[ 1].inputs[0].keyframe_delete('default_value') elif engine == 'BLENDER_RENDER': matCl.keyframe_delete('diffuse_color') except: pass start += 1 return{'FINISHED'} except Exception as e: error_handlers(self, "object.colorblenderclear", e, "Reset Keyframes could not be completed") return {'CANCELLED'} # F-Curve Noise # will add noise modifiers to each selected object f-curves # change type to: 'rotation' | 'location' | 'scale' | '' to effect all # first record a keyframe for this to work (to generate the f-curves) class OBJECT_OT_fcnoise(Operator): bl_idname = "object.btfcnoise" bl_label = "Btrace: F-curve Noise" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): try: Btrace = context.window_manager.curve_tracer amp = Btrace.fcnoise_amp timescale = Btrace.fcnoise_timescale addkeyframe = Btrace.fcnoise_key # This sets properties for Loc, Rot and Scale # if they're checked in the Tools window noise_rot = 'rotation' noise_loc = 'location' noise_scale = 'scale' if not Btrace.fcnoise_rot: noise_rot = 'none' if not Btrace.fcnoise_loc: noise_loc = 'none' if not Btrace.fcnoise_scale: noise_scale = 'none' # Add settings from panel for type of keyframes types = noise_loc, noise_rot, noise_scale amplitude = amp time_scale = timescale for i in context.selected_objects: # Add keyframes, this is messy and should only # add keyframes for what is checked if addkeyframe is True: bpy.ops.anim.keyframe_insert(type="LocRotScale") for obj in context.selected_objects: if obj.animation_data: for c in obj.animation_data.action.fcurves: if c.data_path.startswith(types): # clean modifiers for m in c.modifiers: c.modifiers.remove(m) # add noide modifiers n = c.modifiers.new('NOISE') n.strength = amplitude n.scale = time_scale n.phase = rand_randint(0, 999) return {'FINISHED'} except Exception as e: error_handlers(self, "object.btfcnoise", e, "F-curve Noise could not be completed") return {'CANCELLED'} # Curve Grow Animation # Animate curve radius over length of time class OBJECT_OT_curvegrow(Operator): bl_idname = "curve.btgrow" bl_label = "Run Script" bl_description = "Keyframe points radius" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.object and context.object.type in {'CURVE'}) def execute(self, context): try: # not so nice with the nested try blocks, however the inside one # is used as a switch statement Btrace = context.window_manager.curve_tracer anim_f_start, anim_length, anim_auto = Btrace.anim_f_start, \ Btrace.anim_length, \ Btrace.anim_auto curve_resolution, curve_depth = Btrace.curve_resolution, \ Btrace.curve_depth # make the curve visible objs = context.selected_objects # Execute on multiple selected objects for i in objs: context.view_layer.objects.active = i obj = context.active_object try: obj.data.fill_mode = 'FULL' except: obj.data.dimensions = '3D' obj.data.fill_mode = 'FULL' if not obj.data.bevel_resolution: obj.data.bevel_resolution = curve_resolution if not obj.data.bevel_depth: obj.data.bevel_depth = curve_depth if anim_auto: anim_f_start = bpy.context.scene.frame_start anim_length = bpy.context.scene.frame_end # get points data and beautify actual, total = anim_f_start, 0 for sp in obj.data.splines: total += len(sp.points) + len(sp.bezier_points) step = anim_length / total for sp in obj.data.splines: sp.radius_interpolation = 'BSPLINE' po = [p for p in sp.points] + [p for p in sp.bezier_points] if not Btrace.anim_keepr: for p in po: p.radius = 1 if Btrace.anim_tails and not sp.use_cyclic_u: po[0].radius = po[-1].radius = 0 po[1].radius = po[-2].radius = .65 ra = [p.radius for p in po] # record the keyframes for i in range(len(po)): po[i].radius = 0 po[i].keyframe_insert('radius', frame=actual) actual += step po[i].radius = ra[i] po[i].keyframe_insert( 'radius', frame=(actual + Btrace.anim_delay) ) if Btrace.anim_f_fade: po[i].radius = ra[i] po[i].keyframe_insert( 'radius', frame=(actual + Btrace.anim_f_fade - step) ) po[i].radius = 0 po[i].keyframe_insert( 'radius', frame=(actual + Btrace.anim_delay + Btrace.anim_f_fade) ) bpy.context.scene.frame_set(Btrace.anim_f_start) return{'FINISHED'} except Exception as e: error_handlers(self, "curve.btgrow", e, "Grow curve could not be completed") return {'CANCELLED'} # Remove animation and curve radius data class OBJECT_OT_reset(Operator): bl_idname = "object.btreset" bl_label = "Clear animation" bl_description = "Remove animation / curve radius data" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): try: objs = context.selected_objects for i in objs: # Execute on multiple selected objects context.view_layer.objects.active = i obj = context.active_object obj.animation_data_clear() if obj.type == 'CURVE': for sp in obj.data.splines: po = [p for p in sp.points] + [p for p in sp.bezier_points] for p in po: p.radius = 1 return{'FINISHED'} except Exception as e: error_handlers(self, "object.btreset", e, "Clear animation could not be completed") return {'CANCELLED'}