bl_info = { "name": "Snowflake Generator", "author": "Eoin Brennan (Mayeoin Bread)", "version": (1, 3, 1), "blender": (2, 74, 0), "location": "View3D > Add > Mesh", "descrition": "Construct a randomly generated snowflake", "warning": "", "wiki_url": "", "tracker_url": "", "category": "Add Mesh" } import bpy import bmesh from mathutils import ( Matrix, Vector, ) from bpy.types import ( Operator, Menu, ) from bpy.props import ( BoolProperty, FloatProperty, IntProperty, ) from math import ( pi, sin, cos, acos, ) from random import ( randint, uniform, ) from bl_operators.presets import AddPresetBase class MESH_MT_snowflakes_presets(Menu): '''Presets for mesh.snowflake''' bl_label = "Spiral Curve Presets" bl_idname = "MESH_MT_snowflakes_presets" preset_subdir = "mesh_snowflake_presets/mesh.snowflake" preset_operator = "script.execute_preset" draw = bpy.types.Menu.draw_preset class SnowflakeGen_presets(AddPresetBase, Operator): bl_idname = "mesh.snowflake_presets" bl_label = "Snowflakes" bl_description = "Snowflakes Presets" preset_menu = "MESH_MT_snowflakes_presets" preset_subdir = "mesh_snowflake_presets/mesh.snowflake" preset_defines = [ "op = bpy.context.active_operator", ] preset_values = [ "op.rnums", "op.randlegs", "op.randInternals", "op.ds", "op.dds", "op.randos", "op.radius", "op.numV", "op.fill", "op.numR", "op.updateS" ] class SnowflakeGen(Operator): bl_idname = "mesh.snowflake" bl_label = "Snowflake" bl_description = ("Construct snowflakes with randomized shapes.\n" "Note: It will replace the existing active Mesh Object") bl_options = {"REGISTER", "UNDO"} numR = IntProperty( name="Outer rings", description="Number of outer rings", default=1, min=0, max=4 ) rnums = IntProperty( name="Random value seed 1", default=0, min=0, max=20, options={"HIDDEN"} ) randlegs = IntProperty( name="Random value seed 2", default=0, min=0, max=20, options={"HIDDEN"} ) randInternals = IntProperty( name="Random value seed 3", default=0, min=0, max=20, options={"HIDDEN"} ) randos = FloatProperty( name="Random seed value 4", description="Size of the initial circle", min=0.2, max=10.0, precision=9, default=1.0, options={"HIDDEN"} ) ds = FloatProperty( name="Random seed value 5", min=0.2, max=10.0, precision=9, default=1.0, options={"HIDDEN"} ) dds = FloatProperty( name="Random seed value 6", min=0.2, max=10.0, precision=9, default=1.0, options={"HIDDEN"} ) radius = FloatProperty( name="Base Radius", description="Size of the initial circle", min=0.1, max=3.0, precision=4, default=1.0 ) numV = IntProperty( name="Vertices", description="Number of vertices around the circle", default=6, min=3, max=24 ) fill = BoolProperty( name="Fill center", description="Connect the inside circle's vertices with the center one", default=True ) updateS = BoolProperty( name="Update", description="Update the shape based on the current settings used as a seed for randomization", default=True ) presetS = BoolProperty( name="Use preset", description="Update the shape based on the preset used as a seed for randomization", default=False, options={"SKIP_SAVE"} ) def draw(self, context): layout = self.layout row = layout.row(align=True) row.prop(self, "updateS", toggle=True) row.prop(self, "presetS", toggle=True) if self.presetS: row = layout.row(align=True) row.menu("MESH_MT_snowflakes_presets") row.operator("mesh.snowflake_presets", text="", icon='ZOOMIN') row.operator("mesh.snowflake_presets", text="", icon='ZOOMOUT').remove_active = True else: col = layout.column(align=True) col.prop(self, "numR") col.prop(self, "radius") col.prop(self, "numV") col.prop(self, "fill") def addMeshObject(self, context): old_obj = context.active_object if not old_obj or old_obj.type != "MESH": bpy.ops.object.add( type='MESH', enter_editmode=False, view_align=False, location=(0.0, 0.0, 0.0), rotation=(0.0, 0.0, 0.0) ) context.scene.update() obj = context.scene.objects.active obj.name = "Snowflake" return obj return old_obj def invoke(self, context, event): self.updateS = True return self.execute(context) def execute(self, context): if not self.updateS: return {"PASS_THROUGH"} bpy.context.space_data.pivot_point = 'CURSOR' bpy.ops.view3d.snap_cursor_to_center() # --- Nested utility functions START --- # # select a single vert def selVert(par): bm.verts.ensure_lookup_table() bpy.ops.mesh.select_all(action='DESELECT') bm.verts[par].select = True # select a single edge def selEdge(par): bm.edges.ensure_lookup_table() bpy.ops.mesh.select_all(action='DESELECT') bm.edges[par].select = True # select last added vert def selLasVert(): for v in bm.verts: bpy.ops.mesh.select_all(action='DESELECT') v.select = True # select last added edge def selLasEdge(): for e in bm.edges: bpy.ops.mesh.select_all(action='DESELECT') e.select = True # extrude and move along vector def extMov(vec): bpy.ops.mesh.extrude_region_move(TRANSFORM_OT_translate={"value": vec}) # rotate around z-axis origin def rotate(ang): bpy.ops.transform.rotate(value=(-ang), axis=(0.0, 0.0, 0.1)) # subdivide def subdiv(num): if num > 0: bpy.ops.mesh.subdivide(number_cuts=num) # get number of verts def numVerts(): vco = 0 for v in bm.verts: vco += 1 return vco - 1 # get number of edges def numEdges(): eco = 0 for e in bm.edges: eco += 1 return eco - 1 # duplicate and move along vector def dupMove(vec): bpy.ops.mesh.duplicate_move(TRANSFORM_OT_translate={"value": vec}) def extLeg(vert, length, minus): selVert(vert) if minus == 0: legVec = Vector((length * cos(pi / 4), length * sin(pi / 4), 0.0)) extMov(legVec) else: legVec = Vector((-length * cos(pi / 4), length * sin(pi / 4), 0.0)) extMov(legVec) def extLeg2(vert, length, angle): selVert(vert) legVec = Vector((length * sin(angle), length * cos(angle), 0.0)) extMov(legVec) # --- Nested utility functions END --- # obj = None obj = self.addMeshObject(context) if not obj: self.report({'INFO'}, "No Mesh Object found. Operation Cancelled") return {"CANCELLED"} bpy.ops.object.mode_set(mode="OBJECT") bpy.ops.object.mode_set(mode="EDIT") me = obj.data bm = bmesh.from_edit_mesh(me) bm.clear() # Note: this is important - if not done, causes a memory leak on exit bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() # add circle (base for snowflake) if bpy.app.version >= (2, 79, 1): bmesh.ops.create_circle( bm, cap_ends=False, cap_tris=False, segments=self.numV, radius=self.radius, matrix=Matrix(obj.matrix_world), calc_uvs=False ) else: bmesh.ops.create_circle( bm, cap_ends=False, cap_tris=False, segments=self.numV, diameter=self.radius, matrix=Matrix(obj.matrix_world), calc_uvs=False ) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() # variables... rnum = randint(0, 1) if not self.presetS else self.rnums self.rnums = rnum s1, s2 = (1, 2) if rnum == 0 else (2, 1) fillC = self.fill vCount = 0 oVerts = [] rand1 = 2 rando = uniform(0.2, 1.0) * 2 if not self.presetS else self.randos self.randos = rando nullV = Vector((0.0, 0.0, 0.0)) randleg = randint(1, 3) if not self.presetS else self.randlegs self.randlegs = randleg randInternal = randint(0, randleg) if not self.presetS else self.randInternals self.randInternals = randInternal numRings = self.numR # count + store number of verts in base circle oVerts = [v.index for v in bm.verts] vCount = len(oVerts) oVerts.pop(0) # vectors for length of edge and angles bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() root = bm.verts[0].co x = bm.verts[1].co y = bm.verts[vCount - 1].co # length of arm d = ((root.x - x.x)**2 + (root.y - x.y)**2)**0.5 x = x + root y = y + root # vector dot product ab = (x.x * y.x) + (x.y * y.y) + (x.z * y.z) aa = ((x.x**2) + (x.y**2) + (x.z**2))**0.5 bb = ((y.x**2) + (y.y**2) + (y.z**2))**0.5 angle = acos(ab / (aa * bb)) # Extrude top vert upwards selVert(0) myVec = Vector((0.0, d * rando * 2, 0.0)) extMov(myVec) # Subdivide selLasEdge() subdiv(1) # Save d, randomise new d for legs dd = d if not self.presetS else self.dds d = d * uniform(0.4, 1.0) if not self.presetS else self.ds self.ds = d self.dds = dd # Position new vert bm.verts.ensure_lookup_table() lastVert = numVerts() bm.verts[lastVert].co = root bm.verts[lastVert].co.y = root.y + (vCount - 1) * (d * rando * 2) / vCount # Extrude right leg extLeg(lastVert, (4 * dd / d) / vCount, 0) # Extrude left leg bm.verts.ensure_lookup_table() extLeg(lastVert, (4 * dd / d) / vCount, 1) # if d < 0.8 * dd, add 1 or two more sets of legs on end # Extrude outer ring ringEdge = numEdges() - 2 if d < 0.6 * dd: mlva = [] lastEdge = numEdges() selEdge(ringEdge - 1) subdiv(randleg) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() lastVert = numVerts() for l in range(randleg): mlva.append(bm.verts[lastVert].index - l) hg = (4 * dd / d) / vCount - len(mlva) * (d / 2) for l in range(len(mlva)): bm.verts.ensure_lookup_table() extLeg(mlva[l], hg, 0) bm.verts.ensure_lookup_table() extLeg(mlva[l], hg, 1) hg = hg + d / 2 s3 = 0 else: s3 = 1 # Extrude outer ring lastEdge = numEdges() selEdge(ringEdge) subdiv(numRings) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() lastVert = numVerts() outerRingLoops = [] for i in range(numRings): outerRingLoops.append(lastVert - i) for j in range(len(outerRingLoops)): selVert(outerRingLoops[j]) # extrude and rotate based on rand1 number for i in range(rand1): bm.verts.ensure_lookup_table() extMov(nullV) rotate(angle / rand1) lastVert = numVerts() # if it's the inner ring if j == 0: # select middle vert selVert(lastVert - 1) upVec = Vector((0.0, dd * rando * 2, 0.0)) # snap cursor to rotate around the vert bpy.ops.view3d.snap_cursor_to_selected() # extrude and rotate around vert bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() extMov(upVec) rotate(angle / 2) bpy.ops.view3d.snap_cursor_to_center() selLasEdge() hg = d / s1 if s3 == 0: subdiv(s1) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() lastVert = numVerts() extLeg2(lastVert, hg, angle / 2 + (pi / 4)) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() extLeg2(lastVert, hg, angle / 2 - (pi / 4)) lastEdge = numEdges() if s1 == 1: if randInternal == 0: selEdge(lastEdge - 2) else: selEdge(lastEdge - 3) else: if randInternal == 0: selEdge(lastEdge - 4) else: selEdge(lastEdge - 3) subdiv(randleg) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() lastVert = numVerts() mlvb = [] for l in range(randleg): bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() mlvb.append(bm.verts[lastVert].index - l) randrev = randint(0, 1) if randrev == 1: mlvb.reverse() for l in range(len(mlvb)): hg = hg - (d / (randleg + 1)) extLeg2(mlvb[l], hg, (angle / 2) + (pi / 4)) extLeg2(mlvb[l], hg, (angle / 2) - (pi / 4)) # if it's second ring if j == 1: # select two verts # rest as before except with two legs this time selVert(lastVert - 1) upVec = Vector((0.0, dd * rando * 2, 0.0)) bpy.ops.view3d.snap_cursor_to_selected() extMov(upVec) rotate(2 * angle / 3) selVert(lastVert - 2) bpy.ops.view3d.snap_cursor_to_selected() extMov(upVec) rotate(angle / 3) bpy.ops.view3d.snap_cursor_to_center() lasEdg = numEdges() lledg = lasEdg - 1 selEdge(lasEdg) subdiv(s2) if s3 == 1: lastVert = numVerts() extLeg2(lastVert, (d / dd) / s1, angle / 3 + (pi / 4)) extLeg2(lastVert, (d / dd) / s1, angle / 3 - (pi / 4)) lastEdge = numEdges() - 4 selEdge(lledg) subdiv(s2) if s3 == 1: lastVert = numVerts() extLeg2(lastVert, (d / dd) / s1, 2 * angle / 3 + (pi / 4)) extLeg2(lastVert, (d / dd) / s1, 2 * angle / 3 - (pi / 4)) lastEdge = numEdges() - 4 rand1 = rand1 + 1 # Select all new verts and duplicate bpy.ops.mesh.select_all(action='SELECT') bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() for o in oVerts: bm.verts[o].select = False for i in range(vCount - 1): # duplicate around origin dupMove(nullV) rotate(angle) dupVert = [] dupEdge = [] for v in bm.verts: if v.select: dupVert.append(v.index) for e in bm.edges: if e.select: dupEdge.append(e.index) bpy.ops.mesh.select_all(action='DESELECT') for r in range(len(dupVert)): bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.verts[dupVert[r]].select = True for r in range(len(dupEdge)): bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.edges[dupEdge[r]].select = True # Fill center if fillC: selVert(0) for o in oVerts: bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.verts[o].select = True extMov(nullV) bpy.ops.view3d.snap_selected_to_cursor(use_offset=False) # Remove doubles bpy.ops.mesh.select_all(action='SELECT') bpy.ops.mesh.remove_doubles(threshold=0.003) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bmesh.update_edit_mesh(me, destructive=True) self.updateS = False return {'FINISHED'} def menu_func(self, context): self.layout.operator(SnowflakeGen.bl_idname, text="Snowflake") def register(): bpy.utils.register_class(SnowflakeGen) bpy.utils.register_class(MESH_MT_snowflakes_presets) bpy.utils.register_class(SnowflakeGen_presets) bpy.types.VIEW3D_MT_mesh_add.append(menu_func) def unregister(): bpy.utils.unregister_class(SnowflakeGen) bpy.utils.unregister_class(MESH_MT_snowflakes_presets) bpy.utils.unregister_class(SnowflakeGen_presets) bpy.types.VIEW3D_MT_mesh_add.remove(menu_func) if __name__ == "__main__": register()