# ##### BEGIN GPL LICENSE BLOCK ##### # # SCA Tree Generator, a Blender add-on # (c) 2013 Michel J. Anders (varkenvarken) # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # <pep8 compliant> bl_info = { "name": "SCA Tree Generator", "author": "michel anders (varkenvarken)", "version": (0, 1, 3), "blender": (2, 77, 0), "location": "View3D > Add > Mesh", "description": "Create a tree using the space colonization algorithm starting at the 3D cursor", "warning": "", "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/" "Scripts/Add_Mesh/Add_Space_Tree", "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/", "category": "Add Mesh" } import bpy from random import ( gauss, random, ) from functools import partial from math import ( cos, sin, ) from bpy.props import ( BoolProperty, EnumProperty, FloatProperty, IntProperty, ) from mathutils import ( Euler, Vector, Quaternion, ) # simple skinning algorithm building blocks from .simplefork import ( quadfork, bridgequads, ) # the core class that implements the space colonization # algorithm and the definition of a segment from .sca import ( SCA, Branchpoint, ) from .timer import Timer def availableGroups(self, context): return [(name, name, name, n) for n, name in enumerate(bpy.data.collections.keys())] def availableGroupsOrNone(self, context): groups = [('None', 'None', 'None', 1)] return groups + [(name, name, name, n + 1) for n, name in enumerate(bpy.data.collections.keys())] def availableObjects(self, context): return [(name, name, name, n + 1) for n, name in enumerate(bpy.data.objects.keys())] def ellipsoid(r=5, rz=5, p=Vector((0, 0, 8)), taper=0): r2 = r * r z2 = rz * rz if rz > r: r = rz while True: x = (random() * 2 - 1) * r y = (random() * 2 - 1) * r z = (random() * 2 - 1) * r f = (z + r) / (2 * r) f = 1 + f * taper if taper >= 0 else (1 - f) * -taper if f * x * x / r2 + f * y * y / r2 + z * z / z2 <= 1: yield p + Vector((x, y, z)) def pointInsideMesh(pointrelativetocursor, ob): # adapted from http://blenderartists.org/forum/showthread.php?" # "195605-Detecting-if-a-point-is-inside-a-mesh-2-5-API&p=1691633&viewfull=1#post1691633 mat = ob.matrix_world.inverted() orig = mat * (pointrelativetocursor + bpy.context.scene.cursor_location) count = 0 axis = Vector((0, 0, 1)) while True: # Note: address changes introduced to object.ray_cast return (see T54414) result, location, normal, index = ob.ray_cast(orig, orig + axis * 10000.0) if index == -1: break count += 1 orig = location + axis * 0.00001 if count % 2 == 0: return False return True def ellipsoid2(rxy=5, rz=5, p=Vector((0, 0, 8)), surfacebias=1, topbias=1): while True: phi = 6.283 * random() theta = 3.1415 * (random() - 0.5) r = random() ** (surfacebias / 2) x = r * rxy * cos(theta) * cos(phi) y = r * rxy * cos(theta) * sin(phi) st = sin(theta) st = (((st + 1) / 2) ** topbias) * 2 - 1 z = r * rz * st # print(">>>%.2f %.2f %.2f "%(x,y,z)) m = p + Vector((x, y, z)) reject = False for ob in bpy.context.selected_objects: # probably we should check if each object is a mesh if pointInsideMesh(m, ob): reject = True break if not reject: yield m def halton3D(index): """ return a quasi random 3D vector R3 in [0,1]. each component is based on a halton sequence. quasi random is good enough for our purposes and is more evenly distributed then pseudo random sequences. See en.m.wikipedia.org/wiki/Halton_sequence """ def halton(index, base): result = 0 f = 1.0 / base I = index while I > 0: result += f * (I % base) I = int(I / base) f /= base return result return Vector((halton(index, 2), halton(index, 3), halton(index, 5))) def insidegroup(pointrelativetocursor, group): if bpy.data.collections.find(group) < 0: return False for ob in bpy.data.collections[group].objects: if pointInsideMesh(pointrelativetocursor, ob): return True return False def groupdistribution(crowngroup, shadowgroup=None, seed=0, size=Vector((1, 1, 1)), pointrelativetocursor=Vector((0, 0, 0))): if crowngroup == shadowgroup: shadowgroup = None # safeguard otherwise every marker would be rejected nocrowngroup = bpy.data.collections.find(crowngroup) < 0 noshadowgroup = (shadowgroup is None) or (bpy.data.collections.find(shadowgroup) < 0) or (shadowgroup == 'None') index = 100 + seed nmarkers = 0 nyield = 0 while True: nmarkers += 1 v = halton3D(index) v[0] *= size[0] v[1] *= size[1] v[2] *= size[2] v += pointrelativetocursor index += 1 insidecrown = nocrowngroup or insidegroup(v, crowngroup) outsideshadow = noshadowgroup or not insidegroup(v, shadowgroup) # if shadowgroup overlaps all or a significant part of the crowngroup # no markers will be yielded and we would be in an endless loop. # so if we yield too few correct markers we start yielding them anyway. lowyieldrate = (nmarkers > 200) and (nyield / nmarkers < 0.01) if (insidecrown and outsideshadow) or lowyieldrate: nyield += 1 yield v def groupExtends(group): """ return a size,minimum tuple both Vector elements, describing the size and position of the bounding box in world space that encapsulates all objects in a group. """ bb = [] if bpy.data.collections.find(group) >= 0: for ob in bpy.data.collections[group].objects: rot = ob.matrix_world.to_quaternion() scale = ob.matrix_world.to_scale() translate = ob.matrix_world.translation for v in ob.bound_box: # v is not a vector but an array of floats p = ob.matrix_world * Vector(v[0:3]) bb.extend(p[0:3]) mx = Vector((max(bb[0::3]), max(bb[1::3]), max(bb[2::3]))) mn = Vector((min(bb[0::3]), min(bb[1::3]), min(bb[2::3]))) return mx - mn, mn def createLeaves(tree, probability=0.5, size=0.5, randomsize=0.1, randomrot=0.1, maxconnections=2, bunchiness=1.0, connectoffset=-0.1): p = bpy.context.scene.cursor_location verts = [] faces = [] c1 = Vector((connectoffset, -size / 2, 0)) c2 = Vector((size + connectoffset, -size / 2, 0)) c3 = Vector((size + connectoffset, size / 2, 0)) c4 = Vector((connectoffset, size / 2, 0)) t = gauss(1.0 / probability, 0.1) bpswithleaves = 0 for bp in tree.branchpoints: if bp.connections < maxconnections: dv = tree.branchpoints[bp.parent].v - bp.v if bp.parent else Vector((0, 0, 0)) dvp = Vector((0, 0, 0)) bpswithleaves += 1 nleavesonbp = 0 while t < bpswithleaves: nleavesonbp += 1 rx = (random() - 0.5) * randomrot * 6.283 # TODO vertical tilt in direction of tropism ry = (random() - 0.5) * randomrot * 6.283 rot = Euler((rx, ry, random() * 6.283), 'ZXY') scale = 1 + (random() - 0.5) * randomsize v = c1.copy() v.rotate(rot) verts.append(v * scale + bp.v + dvp) v = c2.copy() v.rotate(rot) verts.append(v * scale + bp.v + dvp) v = c3.copy() v.rotate(rot) verts.append(v * scale + bp.v + dvp) v = c4.copy() v.rotate(rot) verts.append(v * scale + bp.v + dvp) n = len(verts) faces.append((n - 1, n - 4, n - 3, n - 2)) # this is not the best choice of distribution because we might # get negative values especially if sigma is large t += gauss(1.0 / probability, 0.1) dvp = nleavesonbp * (dv / (probability ** bunchiness)) # TODO add some randomness to the offset mesh = bpy.data.meshes.new('Leaves') mesh.from_pydata(verts, [], faces) mesh.update(calc_edges=True) mesh.uv_textures.new() return mesh def createMarkers(tree, scale=0.05): # not used as markers are parented to tree object that is created at the cursor position # p=bpy.context.scene.cursor_location verts = [] faces = [] tetraeder = [Vector((-1, 1, -1)), Vector((1, -1, -1)), Vector((1, 1, 1)), Vector((-1, -1, 1))] tetraeder = [v * scale for v in tetraeder] tfaces = [(0, 1, 2), (0, 1, 3), (1, 2, 3), (0, 3, 2)] for ep in tree.endpoints: verts.extend([ep + v for v in tetraeder]) n = len(faces) faces.extend([(f1 + n, f2 + n, f3 + n) for f1, f2, f3 in tfaces]) mesh = bpy.data.meshes.new('Markers') mesh.from_pydata(verts, [], faces) mesh.update(calc_edges=True) return mesh def createObjects(tree, parent=None, objectname=None, probability=0.5, size=0.5, randomsize=0.1, randomrot=0.1, maxconnections=2, bunchiness=1.0): if (parent is None) or (objectname is None) or (objectname == 'None'): return # not necessary, we parent the new objects: p=bpy.context.scene.cursor_location theobject = bpy.data.objects[objectname] t = gauss(1.0 / probability, 0.1) bpswithleaves = 0 for bp in tree.branchpoints: if bp.connections < maxconnections: dv = tree.branchpoints[bp.parent].v - bp.v if bp.parent else Vector((0, 0, 0)) dvp = Vector((0, 0, 0)) bpswithleaves += 1 nleavesonbp = 0 while t < bpswithleaves: nleavesonbp += 1 rx = (random() - 0.5) * randomrot * 6.283 # TODO vertical tilt in direction of tropism ry = (random() - 0.5) * randomrot * 6.283 rot = Euler((rx, ry, random() * 6.283), 'ZXY') scale = size + (random() - 0.5) * randomsize # add new object and parent it obj = bpy.data.objects.new(objectname, theobject.data) obj.location = bp.v + dvp obj.rotation_mode = 'ZXY' obj.rotation_euler = rot[:] obj.scale = [scale, scale, scale] obj.parent = parent bpy.context.collection.objects.link(obj) # this is not the best choice of distribution because we might # get negative values especially if sigma is large t += gauss(1.0 / probability, 0.1) dvp = nleavesonbp * (dv / (probability ** bunchiness)) # TODO add some randomness to the offset def vertextend(v, dv): n = len(v) v.extend(dv) return tuple(range(n, n + len(dv))) def vertcopy(loopa, v, p): dv = [v[i] + p for i in loopa] # print(loopa, p, dv) return vertextend(v, dv) def bend(p0, p1, p2, loopa, loopb, verts): # will extend this with a tri centered at p0 # print('bend') return bridgequads(loopa, loopb, verts) def extend(p0, p1, p2, loopa, verts): # will extend this with a tri centered at p0 # print('extend') # print(p0,p1,p2,[verts[i] for i in loopa]) # both difference point upward, we extend to the second d1 = p1 - p0 d2 = p0 - p2 p = (verts[loopa[0]] + verts[loopa[1]] + verts[loopa[2]] + verts[loopa[3]]) / 4 a = d1.angle(d2, 0) if abs(a) < 0.05: # print('small angle') loopb = vertcopy(loopa, verts, p0 - d2 / 2 - p) # all verts in loopb are displaced the same amount so no need to find the minimum distance n = 4 return ([(loopa[(i) % n], loopa[(i + 1) % n], loopb[(i + 1) % n], loopb[(i) % n]) for i in range(n)], loopa, loopb) r = d2.cross(d1) q = Quaternion(r, -a) dverts = [verts[i] - p for i in loopa] # print('large angle',dverts,'axis',r) for dv in dverts: dv.rotate(q) # print('rotated',dverts) for dv in dverts: dv += (p0 - d2 / 2) # print('moved',dverts) loopb = vertextend(verts, dverts) # none of the verts in loopb are rotated so no need to find the minimum distance n = 4 return ([(loopa[(i) % n], loopa[(i + 1) % n], loopb[(i + 1) % n], loopb[(i) % n]) for i in range(n)], loopa, loopb) def nonfork(bp, parent, apex, verts, p, branchpoints): # print('nonfork bp ',bp.index,bp.v,bp.loop if hasattr(bp,'loop') else None) # print('nonfork parent',parent.index,parent.v,parent.loop if hasattr(parent,'loop') else None) # print('nonfork apex ',apex.index,apex.v,apex.loop if hasattr(apex,'loop') else None) if hasattr(bp, 'loop'): if hasattr(apex, 'loop'): # print('nonfork bend bp->apex') return bend(bp.v + p, parent.v + p, apex.v + p, bp.loop, apex.loop, verts) else: # print('nonfork extend bp->apex') faces, loop1, loop2 = extend(bp.v + p, parent.v + p, apex.v + p, bp.loop, verts) apex.loop = loop2 return faces, loop1, loop2 else: if hasattr(parent, 'loop'): # print('nonfork extend from bp->parent') # faces,loop1,loop2 = extend(bp.v+p, apex.v+p, parent.v+p, parent.loop, verts) if parent.parent is None: return None, None, None grandparent = branchpoints[parent.parent] faces, loop1, loop2 = extend(grandparent.v + p, parent.v + p, bp.v + p, parent.loop, verts) bp.loop = loop2 return faces, loop1, loop2 else: # print('nonfork no loop') # neither parent nor apex already have a loop calculated # will fill this later ... return None, None, None def endpoint(bp, parent, verts, p): # extrapolate to tip of branch. we do not close the tip for now faces, loop1, loop2 = extend(bp.v + p, parent.v + p, bp.v + (bp.v - parent.v) + p, bp.loop, verts) return faces, loop1, loop2 def root(bp, apex, verts, p): # extrapolate non-forked roots faces, loop1, loop2 = extend(bp.v + p, bp.v - (apex.v - bp.v) + p, apex.v + p, bp.loop, verts) apex.loop = loop2 return faces, loop1, loop2 def skin(aloop, bloop, faces): n = len(aloop) for i in range(n): faces.append((aloop[i], aloop[(i + 1) % n], bloop[(i + 1) % n], bloop[i])) def createGeometry(tree, power=0.5, scale=0.01, addleaves=False, pleaf=0.5, leafsize=0.5, leafrandomsize=0.1, leafrandomrot=0.1, nomodifiers=True, skinmethod='NATIVE', subsurface=False, maxleafconnections=2, bleaf=1.0, connectoffset=-0.1, timeperf=True): timings = Timer() p = bpy.context.scene.cursor_location verts = [] edges = [] faces = [] radii = [] roots = set() # Loop over all branchpoints and create connected edges for n, bp in enumerate(tree.branchpoints): verts.append(bp.v + p) radii.append(bp.connections) bp.index = n if not (bp.parent is None): edges.append((len(verts) - 1, bp.parent)) else: nv = len(verts) roots.add(nv - 1) timings.add('skeleton') # native skinning method if nomodifiers is False and skinmethod == 'NATIVE': # add a quad edge loop to all roots for r in roots: rootp = verts[r] nv = len(verts) radius = 0.7071 * ((tree.branchpoints[r].connections + 1) ** power) * scale verts.extend( [rootp + Vector((-radius, -radius, 0)), rootp + Vector((radius, -radius, 0)), rootp + Vector((radius, radius, 0)), rootp + Vector((-radius, radius, 0))] ) tree.branchpoints[r].loop = (nv, nv + 1, nv + 2, nv + 3) # print('root verts',tree.branchpoints[r].loop) # faces.append((nv, nv + 1,nv + 2)) edges.extend([(nv, nv + 1), (nv + 1, nv + 2), (nv + 2, nv + 3), (nv + 3, nv)]) # skin all forked branchpoints, no attempt is yet made to adjust the radius forkfork = set() for bpi, bp in enumerate(tree.branchpoints): if not(bp.apex is None or bp.shoot is None): apex = tree.branchpoints[bp.apex] shoot = tree.branchpoints[bp.shoot] p0 = bp.v r0 = ((bp.connections + 1) ** power) * scale p2 = apex.v r2 = ((apex.connections + 1) ** power) * scale p3 = shoot.v r3 = ((shoot.connections + 1) ** power) * scale if bp.parent is not None: parent = tree.branchpoints[bp.parent] p1 = parent.v r1 = (parent.connections ** power) * scale else: p1 = p0 - (p2 - p0) r1 = r0 skinverts, skinfaces = quadfork(p0, p1, p2, p3, r0, r1, r2, r3) nv = len(verts) verts.extend([v + p for v in skinverts]) faces.extend([tuple(v + nv for v in f) for f in skinfaces]) # the vertices of the quads at the end of the internodes # are returned as the first 12 vertices of a total of 22 # we store them for reuse by non-forked internodes but # first check if we have a fork to fork connection nv = len(verts) if hasattr(bp, 'loop') and not (bpi in forkfork): # already assigned by another fork faces.extend(bridgequads(bp.loop, [nv - 22, nv - 21, nv - 20, nv - 19], verts)[0]) forkfork.add(bpi) else: bp.loop = [nv - 22, nv - 21, nv - 20, nv - 19] # already assigned by another fork but not yet skinned if hasattr(apex, 'loop') and not (bp.apex in forkfork): faces.extend(bridgequads(apex.loop, [nv - 18, nv - 17, nv - 16, nv - 15], verts)[0]) forkfork.add(bp.apex) else: apex.loop = [nv - 18, nv - 17, nv - 16, nv - 15] # already assigned by another fork but not yet skinned if hasattr(shoot, 'loop') and not (bp.shoot in forkfork): faces.extend(bridgequads(shoot.loop, [nv - 14, nv - 13, nv - 12, nv - 11], verts)[0]) forkfork.add(bp.shoot) else: shoot.loop = [nv - 14, nv - 13, nv - 12, nv - 11] # skin the roots that are not forks for r in roots: bp = tree.branchpoints[r] if bp.apex is not None and bp.parent is None and bp.shoot is None: bfaces, apexloop, parentloop = root(bp, tree.branchpoints[bp.apex], verts, p) if bfaces is not None: faces.extend(bfaces) # skin all non-forking branchpoints, that is those not a root or and endpoint skinnednonforks = set() start = -1 while(start != len(skinnednonforks)): start = len(skinnednonforks) # print('-' * 20, start) for bp in tree.branchpoints: if bp.shoot is None and not (bp.parent is None or bp.apex is None or bp in skinnednonforks): bfaces, apexloop, parentloop = nonfork( bp, tree.branchpoints[bp.parent], tree.branchpoints[bp.apex], verts, p, tree.branchpoints ) if bfaces is not None: # print(bfaces,apexloop,parentloop) faces.extend(bfaces) skinnednonforks.add(bp) # skin endpoints for bp in tree.branchpoints: if bp.apex is None and bp.parent is not None: bfaces, apexloop, parentloop = endpoint(bp, tree.branchpoints[bp.parent], verts, p) if bfaces is not None: faces.extend(bfaces) # end of native skinning section timings.add('nativeskin') # create the tree mesh mesh = bpy.data.meshes.new('Tree') mesh.from_pydata(verts, edges, faces) mesh.update(calc_edges=True) # create the tree object an make it the only selected and active object in the scene obj_new = bpy.data.objects.new(mesh.name, mesh) base = bpy.context.collection.objects.link(obj_new) for ob in bpy.context.scene.objects: ob.select_set(False) base.select_set(True) bpy.context.view_layer.objects.active = obj_new bpy.ops.object.origin_set(type='ORIGIN_CURSOR') timings.add('createmesh') # add a subsurf modifier to smooth the branches if nomodifiers is False: if subsurface: bpy.ops.object.modifier_add(type='SUBSURF') bpy.context.active_object.modifiers[0].levels = 1 bpy.context.active_object.modifiers[0].render_levels = 1 # add a skin modifier if skinmethod == 'BLENDER': bpy.ops.object.modifier_add(type='SKIN') bpy.context.active_object.modifiers[-1].use_smooth_shade = True bpy.context.active_object.modifiers[-1].use_x_symmetry = True bpy.context.active_object.modifiers[-1].use_y_symmetry = True bpy.context.active_object.modifiers[-1].use_z_symmetry = True skinverts = bpy.context.active_object.data.skin_vertices[0].data for i, v in enumerate(skinverts): v.radius = [(radii[i] ** power) * scale, (radii[i] ** power) * scale] if i in roots: v.use_root = True # add an extra subsurf modifier to smooth the skin bpy.ops.object.modifier_add(type='SUBSURF') bpy.context.active_object.modifiers[-1].levels = 1 bpy.context.active_object.modifiers[-1].render_levels = 2 timings.add('modifiers') # create the leaves object if addleaves: mesh = createLeaves( tree, pleaf, leafsize, leafrandomsize, leafrandomrot, maxleafconnections, bleaf, connectoffset ) obj_leaves = bpy.data.objects.new(mesh.name, mesh) base = bpy.context.collection.objects.link(obj_leaves) obj_leaves.parent = obj_new bpy.context.view_layer.objects.active = obj_leaves bpy.ops.object.origin_set(type='ORIGIN_CURSOR') bpy.context.view_layer.objects.active = obj_new timings.add('leaves') if timeperf: print(timings) return obj_new class SCATree(bpy.types.Operator): bl_idname = "mesh.sca_tree" bl_label = "SCATree" bl_description = "Generate a tree using a space colonization algorithm" bl_options = {'REGISTER', 'UNDO', 'PRESET'} internodeLength: FloatProperty( name="Internode Length", description="Internode length in Blender Units", default=0.75, min=0.01, soft_max=3.0, subtype='DISTANCE', unit='LENGTH' ) killDistance: FloatProperty( name="Kill Distance", description="Kill Distance as a multiple of the internode length", default=3, min=0.01, soft_max=100.0 ) influenceRange: FloatProperty( name="Influence Range", description="Influence Range as a multiple of the internode length", default=15, min=0.01, soft_max=100.0 ) tropism: FloatProperty( name="Tropism", description="The tendency of branches to bend up or down", default=0, min=-1.0, soft_max=1.0 ) power: FloatProperty( name="Power", description="Tapering power of branch connections", default=0.3, min=0.01, soft_max=1.0 ) scale: FloatProperty( name="Scale", description="Branch size", default=0.01, min=0.0001, soft_max=1.0 ) # the group related properties are not saved as presets because on reload # no groups with the same names might exist, causing an exception useGroups: BoolProperty( name="Use object groups", options={'ANIMATABLE', 'SKIP_SAVE'}, description="Use groups of objects to specify marker distribution", default=False ) crownGroup: EnumProperty( items=availableGroups, options={'ANIMATABLE', 'SKIP_SAVE'}, name='Crown Group', description='Group of objects that specify crown shape' ) shadowGroup: EnumProperty( items=availableGroupsOrNone, options={'ANIMATABLE', 'SKIP_SAVE'}, name='Shadow Group', description='Group of objects subtracted from the crown shape' ) exclusionGroup: EnumProperty( items=availableGroupsOrNone, options={'ANIMATABLE', 'SKIP_SAVE'}, name='Exclusion Group', description='Group of objects that will not be penetrated by growing branches' ) useTrunkGroup: BoolProperty( name="Use trunk group", options={'ANIMATABLE', 'SKIP_SAVE'}, description="Use the locations of a group of objects to " "specify trunk starting points instead of 3d cursor", default=False ) trunkGroup: EnumProperty( items=availableGroups, options={'ANIMATABLE', 'SKIP_SAVE'}, name='Trunk Group', description='Group of objects whose locations specify trunk starting points' ) crownSize: FloatProperty( name="Crown Size", description="Crown size", default=5, min=1, soft_max=29 ) crownShape: FloatProperty( name="Crown Shape", description="Crown shape", default=1, min=0.2, soft_max=5 ) crownOffset: FloatProperty( name="Crown Offset", description="Crown offset (the length of the bole)", default=3, min=0, soft_max=20.0 ) surfaceBias: FloatProperty( name="Surface Bias", description="Surface bias (how much markers are favored near the surface)", default=1, min=-10, soft_max=10 ) topBias: FloatProperty( name="Top Bias", description="Top bias (how much markers are favored near the top)", default=1, min=-10, soft_max=10 ) randomSeed: IntProperty( name="Random Seed", description="The seed governing random generation", default=0, min=0 ) maxIterations: IntProperty( name="Maximum Iterations", description="The maximum number of iterations allowed for tree generation", default=40, min=0 ) numberOfEndpoints: IntProperty( name="Number of Endpoints", description="The number of endpoints generated in the growing volume", default=100, min=0 ) newEndPointsPer1000: IntProperty( name="Number of new Endpoints", description="The number of new endpoints generated in the growing volume per thousand iterations", default=0, min=0 ) maxTime: FloatProperty( name="Maximum Time", description=("The maximum time to run the generation for " "in seconds/generation (0.0 = Disabled). Currently ignored"), default=0.0, min=0.0, soft_max=10 ) pLeaf: FloatProperty( name="Leaves per internode", description=("The average number of leaves per internode"), default=0.5, min=0.0, soft_max=4 ) bLeaf: FloatProperty( name="Leaf clustering", description=("How much leaves cluster to the end of the internode"), default=1, min=1, soft_max=4 ) leafSize: FloatProperty( name="Leaf Size", description=("The leaf size"), default=0.5, min=0.0, soft_max=1 ) leafRandomSize: FloatProperty( name="Leaf Random Size", description=("The amount of randomness to add to the leaf size"), default=0.1, min=0.0, soft_max=10 ) leafRandomRot: FloatProperty( name="Leaf Random Rotation", description=("The amount of random rotation to add to the leaf"), default=0.1, min=0.0, soft_max=1 ) connectoffset: FloatProperty( name="Connect Offset", description=("Offset of leaf to twig"), default=-0.1 ) leafMaxConnections: IntProperty( name="Max Connections", description="The maximum number of connections of an internode elegible for a leaf", default=2, min=0 ) addLeaves: BoolProperty( name="Add Leaves", default=False ) objectName: EnumProperty( items=availableObjects, options={'ANIMATABLE', 'SKIP_SAVE'}, name='Object Name', description='Name of additional objects to duplicate at the branchpoints' ) pObject: FloatProperty( name="Objects per internode", description=("The average number of objects per internode"), default=0.3, min=0.0, soft_max=1 ) bObject: FloatProperty( name="Object clustering", description=("How much objects cluster to the end of the internode"), default=1, min=1, soft_max=4 ) objectSize: FloatProperty( name="Object Size", description=("The object size"), default=1, min=0.0, soft_max=2 ) objectRandomSize: FloatProperty( name="Object Random Size", description=("The amount of randomness to add to the object size"), default=0.1, min=0.0, soft_max=10 ) objectRandomRot: FloatProperty( name="Object Random Rotation", description=("The amount of random rotation to add to the object"), default=0.1, min=0.0, soft_max=1 ) objectMaxConnections: IntProperty( name="Max Connections for Object", description="The maximum number of connections of an internode elegible for a object", default=1, min=0 ) addObjects: BoolProperty( name="Add Objects", default=False ) updateTree: BoolProperty( name="Update Tree", default=False ) noModifiers: BoolProperty( name="No Modifers", default=True ) subSurface: BoolProperty( name="Sub Surface", default=False, description="Add subsurface modifier to trunk skin" ) skinMethod: EnumProperty( items=[('NATIVE', 'Native', 'Built in skinning method', 1), ('BLENDER', 'Skin modifier', 'Use Blenders skin modifier', 2)], options={'ANIMATABLE', 'SKIP_SAVE'}, name='Skinning method', description='How to add a surface to the trunk skeleton' ) showMarkers: BoolProperty( name="Show Markers", default=False ) markerScale: FloatProperty( name="Marker Scale", description=("The size of the markers"), default=0.05, min=0.001, soft_max=0.2 ) timePerformance: BoolProperty( name="Time performance", default=False, description="Show duration of generation steps on console" ) @classmethod def poll(self, context): # Check if we are in object mode return context.mode == 'OBJECT' def execute(self, context): if not self.updateTree: return {'PASS_THROUGH'} timings = Timer() # necessary otherwize ray casts toward these objects may fail. # However if nothing is selected, we get a runtime error ... try: bpy.ops.object.mode_set(mode='EDIT', toggle=False) bpy.ops.object.mode_set(mode='OBJECT', toggle=False) except RuntimeError: pass if self.useGroups: size, minp = groupExtends(self.crownGroup) volumefie = partial( groupdistribution, self.crownGroup, self.shadowGroup, self.randomSeed, size, minp - bpy.context.scene.cursor_location ) else: volumefie = partial( ellipsoid2, self.crownSize * self.crownShape, self.crownSize, Vector((0, 0, self.crownSize + self.crownOffset)), self.surfaceBias, self.topBias ) startingpoints = [] if self.useTrunkGroup: if bpy.data.collections.find(self.trunkGroup) >= 0: for ob in bpy.data.collections[self.trunkGroup].objects: p = ob.location - context.scene.cursor_location startingpoints.append(Branchpoint(p, None)) timings.add('scastart') sca = SCA(NBP=self.maxIterations, NENDPOINTS=self.numberOfEndpoints, d=self.internodeLength, KILLDIST=self.killDistance, INFLUENCE=self.influenceRange, SEED=self.randomSeed, TROPISM=self.tropism, volume=volumefie, exclude=lambda p: insidegroup(p, self.exclusionGroup), startingpoints=startingpoints) timings.add('sca') if self.showMarkers: mesh = createMarkers(sca, self.markerScale) obj_markers = bpy.data.objects.new(mesh.name, mesh) base = bpy.context.collection.objects.link(obj_markers) timings.add('showmarkers') sca.iterate2(newendpointsper1000=self.newEndPointsPer1000, maxtime=self.maxTime) timings.add('iterate') obj_new = createGeometry( sca, self.power, self.scale, self.addLeaves, self.pLeaf, self.leafSize, self.leafRandomSize, self.leafRandomRot, self.noModifiers, self.skinMethod, self.subSurface, self.leafMaxConnections, self.bLeaf, self.connectoffset, self.timePerformance ) timings.add('objcreationstart') if self.addObjects: createObjects(sca, obj_new, objectname=self.objectName, probability=self.pObject, size=self.objectSize, randomsize=self.objectRandomSize, randomrot=self.objectRandomRot, maxconnections=self.objectMaxConnections, bunchiness=self.bObject) timings.add('objcreation') if self.showMarkers: obj_markers.parent = obj_new self.updateTree = False if self.timePerformance: timings.add('Total') print(timings) return {'FINISHED'} def draw(self, context): layout = self.layout layout.prop(self, 'updateTree', icon='MESH_DATA') columns = layout.row() col1 = columns.column() col2 = columns.column() box = col1.box() box.label(text="Generation Settings:") box.prop(self, 'randomSeed') box.prop(self, 'maxIterations') box = col1.box() box.label(text="Shape Settings:") box.prop(self, 'numberOfEndpoints') box.prop(self, 'internodeLength') box.prop(self, 'influenceRange') box.prop(self, 'killDistance') box.prop(self, 'power') box.prop(self, 'scale') box.prop(self, 'tropism') newbox = col2.box() newbox.label(text="Crown shape") newbox.prop(self, 'useGroups') if self.useGroups: newbox.label(text="Object groups defining crown shape") groupbox = newbox.box() groupbox.prop(self, 'crownGroup') groupbox = newbox.box() groupbox.alert = (self.shadowGroup == self.crownGroup) groupbox.prop(self, 'shadowGroup') groupbox = newbox.box() groupbox.alert = (self.exclusionGroup == self.crownGroup) groupbox.prop(self, 'exclusionGroup') else: newbox.label(text="Simple ellipsoid defining crown shape") newbox.prop(self, 'crownSize') newbox.prop(self, 'crownShape') newbox.prop(self, 'crownOffset') newbox = col2.box() newbox.prop(self, 'useTrunkGroup') if self.useTrunkGroup: newbox.prop(self, 'trunkGroup') box.prop(self, 'surfaceBias') box.prop(self, 'topBias') box.prop(self, 'newEndPointsPer1000') box = col2.box() box.label(text="Skin options:") box.prop(self, 'noModifiers') if not self.noModifiers: box.prop(self, 'skinMethod') box.prop(self, 'subSurface') layout.prop(self, 'addLeaves', icon='MESH_DATA') if self.addLeaves: box = layout.box() box.label(text="Leaf Settings:") box.prop(self, 'pLeaf') box.prop(self, 'bLeaf') box.prop(self, 'leafSize') box.prop(self, 'leafRandomSize') box.prop(self, 'leafRandomRot') box.prop(self, 'connectoffset') box.prop(self, 'leafMaxConnections') layout.prop(self, 'addObjects', icon='MESH_DATA') if self.addObjects: box = layout.box() box.label(text="Object Settings:") box.prop(self, 'objectName') box.prop(self, 'pObject') box.prop(self, 'bObject') box.prop(self, 'objectSize') box.prop(self, 'objectRandomSize') box.prop(self, 'objectRandomRot') box.prop(self, 'objectMaxConnections') box = layout.box() box.label(text="Debug Settings:") box.prop(self, 'showMarkers') if self.showMarkers: box.prop(self, 'markerScale') box.prop(self, 'timePerformance') def menu_func(self, context): self.layout.operator( SCATree.bl_idname, text="Add Tree to Scene", icon='PLUGIN').updateTree = True def register(): bpy.utils.register_module(__name__) bpy.types.VIEW3D_MT_mesh_add.append(menu_func) def unregister(): bpy.types.VIEW3D_MT_mesh_add.remove(menu_func) bpy.utils.unregister_module(__name__) if __name__ == "__main__": register()