9fc96327d30ac1c981937261e8bba0fb70561219
[blender-addons-contrib.git] / add_mesh_space_tree / __init__.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  SCA Tree Generator, a Blender add-on
4 #  (c) 2013 Michel J. Anders (varkenvarken)
5 #
6 #  This program is free software; you can redistribute it and/or
7 #  modify it under the terms of the GNU General Public License
8 #  as published by the Free Software Foundation; either version 2
9 #  of the License, or (at your option) any later version.
10 #
11 #  This program is distributed in the hope that it will be useful,
12 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #  GNU General Public License for more details.
15 #
16 #  You should have received a copy of the GNU General Public License
17 #  along with this program; if not, write to the Free Software Foundation,
18 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 #
20 # ##### END GPL LICENSE BLOCK #####
21
22 # <pep8 compliant>
23
24 bl_info = {
25     "name": "SCA Tree Generator",
26     "author": "michel anders (varkenvarken)",
27     "version": (0, 1, 3),
28     "blender": (2, 77, 0),
29     "location": "View3D > Add > Mesh",
30     "description": "Create a tree using the space colonization algorithm starting at the 3D cursor",
31     "warning": "",
32     "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
33                 "Scripts/Add_Mesh/Add_Space_Tree",
34     "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/",
35     "category": "Add Mesh"
36 }
37
38 import bpy
39 from random import (
40     gauss, random,
41 )
42 from functools import partial
43 from math import (
44     cos, sin,
45 )
46 from bpy.props import (
47     BoolProperty,
48     EnumProperty,
49     FloatProperty,
50     IntProperty,
51 )
52 from mathutils import (
53     Euler,
54     Vector,
55     Quaternion,
56 )
57 # simple skinning algorithm building blocks
58 from .simplefork import (
59     quadfork, bridgequads,
60 )
61 # the core class that implements the space colonization
62 # algorithm and the definition of a segment
63 from .sca import (
64     SCA, Branchpoint,
65 )
66 from .timer import Timer
67
68
69 def availableGroups(self, context):
70     return [(name, name, name, n) for n, name in enumerate(bpy.data.collections.keys())]
71
72
73 def availableGroupsOrNone(self, context):
74     groups = [('None', 'None', 'None', 1)]
75     return groups + [(name, name, name, n + 1) for n, name in enumerate(bpy.data.collections.keys())]
76
77
78 def availableObjects(self, context):
79     return [(name, name, name, n + 1) for n, name in enumerate(bpy.data.objects.keys())]
80
81
82 def ellipsoid(r=5, rz=5, p=Vector((0, 0, 8)), taper=0):
83     r2 = r * r
84     z2 = rz * rz
85     if rz > r:
86         r = rz
87     while True:
88         x = (random() * 2 - 1) * r
89         y = (random() * 2 - 1) * r
90         z = (random() * 2 - 1) * r
91         f = (z + r) / (2 * r)
92         f = 1 + f * taper if taper >= 0 else (1 - f) * -taper
93         if f * x * x / r2 + f * y * y / r2 + z * z / z2 <= 1:
94             yield p + Vector((x, y, z))
95
96
97 def pointInsideMesh(pointrelativetocursor, ob):
98     # adapted from http://blenderartists.org/forum/showthread.php?"
99     # "195605-Detecting-if-a-point-is-inside-a-mesh-2-5-API&p=1691633&viewfull=1#post1691633
100     mat = ob.matrix_world.inverted()
101     orig = mat * (pointrelativetocursor + bpy.context.scene.cursor_location)
102     count = 0
103     axis = Vector((0, 0, 1))
104     while True:
105         # Note: address changes introduced to object.ray_cast return (see T54414)
106         result, location, normal, index = ob.ray_cast(orig, orig + axis * 10000.0)
107         if index == -1:
108             break
109         count += 1
110         orig = location + axis * 0.00001
111     if count % 2 == 0:
112         return False
113     return True
114
115
116 def ellipsoid2(rxy=5, rz=5, p=Vector((0, 0, 8)), surfacebias=1, topbias=1):
117     while True:
118         phi = 6.283 * random()
119         theta = 3.1415 * (random() - 0.5)
120         r = random() ** (surfacebias / 2)
121         x = r * rxy * cos(theta) * cos(phi)
122         y = r * rxy * cos(theta) * sin(phi)
123         st = sin(theta)
124         st = (((st + 1) / 2) ** topbias) * 2 - 1
125         z = r * rz * st
126         # print(">>>%.2f %.2f %.2f "%(x,y,z))
127         m = p + Vector((x, y, z))
128         reject = False
129         for ob in bpy.context.selected_objects:
130             # probably we should check if each object is a mesh
131             if pointInsideMesh(m, ob):
132                 reject = True
133                 break
134         if not reject:
135             yield m
136
137
138 def halton3D(index):
139     """
140     return a quasi random 3D vector R3 in [0,1].
141     each component is based on a halton sequence.
142     quasi random is good enough for our purposes and is
143     more evenly distributed then pseudo random sequences.
144     See en.m.wikipedia.org/wiki/Halton_sequence
145     """
146
147     def halton(index, base):
148         result = 0
149         f = 1.0 / base
150         I = index
151         while I > 0:
152             result += f * (I % base)
153             I = int(I / base)
154             f /= base
155         return result
156     return Vector((halton(index, 2), halton(index, 3), halton(index, 5)))
157
158
159 def insidegroup(pointrelativetocursor, group):
160     if bpy.data.collections.find(group) < 0:
161         return False
162     for ob in bpy.data.collections[group].objects:
163         if pointInsideMesh(pointrelativetocursor, ob):
164             return True
165     return False
166
167
168 def groupdistribution(crowngroup, shadowgroup=None, seed=0, size=Vector((1, 1, 1)),
169                       pointrelativetocursor=Vector((0, 0, 0))):
170     if crowngroup == shadowgroup:
171         shadowgroup = None  # safeguard otherwise every marker would be rejected
172     nocrowngroup = bpy.data.collections.find(crowngroup) < 0
173     noshadowgroup = (shadowgroup is None) or (bpy.data.collections.find(shadowgroup) < 0) or (shadowgroup == 'None')
174     index = 100 + seed
175     nmarkers = 0
176     nyield = 0
177     while True:
178         nmarkers += 1
179         v = halton3D(index)
180         v[0] *= size[0]
181         v[1] *= size[1]
182         v[2] *= size[2]
183         v += pointrelativetocursor
184         index += 1
185         insidecrown = nocrowngroup or insidegroup(v, crowngroup)
186         outsideshadow = noshadowgroup or not insidegroup(v, shadowgroup)
187         # if shadowgroup overlaps all or a significant part of the crowngroup
188         # no markers will be yielded and we would be in an endless loop.
189         # so if we yield too few correct markers we start yielding them anyway.
190         lowyieldrate = (nmarkers > 200) and (nyield / nmarkers < 0.01)
191         if (insidecrown and outsideshadow) or lowyieldrate:
192             nyield += 1
193             yield v
194
195
196 def groupExtends(group):
197     """
198     return a size,minimum tuple both Vector elements, describing the size and position
199     of the bounding box in world space that encapsulates all objects in a group.
200     """
201     bb = []
202     if bpy.data.collections.find(group) >= 0:
203         for ob in bpy.data.collections[group].objects:
204             rot = ob.matrix_world.to_quaternion()
205             scale = ob.matrix_world.to_scale()
206             translate = ob.matrix_world.translation
207             for v in ob.bound_box:  # v is not a vector but an array of floats
208                 p = ob.matrix_world * Vector(v[0:3])
209                 bb.extend(p[0:3])
210     mx = Vector((max(bb[0::3]), max(bb[1::3]), max(bb[2::3])))
211     mn = Vector((min(bb[0::3]), min(bb[1::3]), min(bb[2::3])))
212     return mx - mn, mn
213
214
215 def createLeaves(tree, probability=0.5, size=0.5, randomsize=0.1,
216                  randomrot=0.1, maxconnections=2, bunchiness=1.0, connectoffset=-0.1):
217     p = bpy.context.scene.cursor_location
218
219     verts = []
220     faces = []
221     c1 = Vector((connectoffset, -size / 2, 0))
222     c2 = Vector((size + connectoffset, -size / 2, 0))
223     c3 = Vector((size + connectoffset, size / 2, 0))
224     c4 = Vector((connectoffset, size / 2, 0))
225     t = gauss(1.0 / probability, 0.1)
226     bpswithleaves = 0
227     for bp in tree.branchpoints:
228         if bp.connections < maxconnections:
229
230             dv = tree.branchpoints[bp.parent].v - bp.v if bp.parent else Vector((0, 0, 0))
231             dvp = Vector((0, 0, 0))
232
233             bpswithleaves += 1
234             nleavesonbp = 0
235             while t < bpswithleaves:
236                 nleavesonbp += 1
237                 rx = (random() - 0.5) * randomrot * 6.283  # TODO vertical tilt in direction of tropism
238                 ry = (random() - 0.5) * randomrot * 6.283
239                 rot = Euler((rx, ry, random() * 6.283), 'ZXY')
240                 scale = 1 + (random() - 0.5) * randomsize
241                 v = c1.copy()
242                 v.rotate(rot)
243                 verts.append(v * scale + bp.v + dvp)
244                 v = c2.copy()
245                 v.rotate(rot)
246                 verts.append(v * scale + bp.v + dvp)
247                 v = c3.copy()
248                 v.rotate(rot)
249                 verts.append(v * scale + bp.v + dvp)
250                 v = c4.copy()
251                 v.rotate(rot)
252                 verts.append(v * scale + bp.v + dvp)
253                 n = len(verts)
254                 faces.append((n - 1, n - 4, n - 3, n - 2))
255                 # this is not the best choice of distribution because we might
256                 # get negative values especially if sigma is large
257                 t += gauss(1.0 / probability, 0.1)
258                 dvp = nleavesonbp * (dv / (probability ** bunchiness))  # TODO add some randomness to the offset
259
260     mesh = bpy.data.meshes.new('Leaves')
261     mesh.from_pydata(verts, [], faces)
262     mesh.update(calc_edges=True)
263     mesh.uv_textures.new()
264     return mesh
265
266
267 def createMarkers(tree, scale=0.05):
268     # not used as markers are parented to tree object that is created at the cursor position
269     # p=bpy.context.scene.cursor_location
270
271     verts = []
272     faces = []
273
274     tetraeder = [Vector((-1, 1, -1)), Vector((1, -1, -1)), Vector((1, 1, 1)), Vector((-1, -1, 1))]
275     tetraeder = [v * scale for v in tetraeder]
276     tfaces = [(0, 1, 2), (0, 1, 3), (1, 2, 3), (0, 3, 2)]
277
278     for ep in tree.endpoints:
279         verts.extend([ep + v for v in tetraeder])
280         n = len(faces)
281         faces.extend([(f1 + n, f2 + n, f3 + n) for f1, f2, f3 in tfaces])
282
283     mesh = bpy.data.meshes.new('Markers')
284     mesh.from_pydata(verts, [], faces)
285     mesh.update(calc_edges=True)
286     return mesh
287
288
289 def createObjects(tree, parent=None, objectname=None, probability=0.5, size=0.5,
290                   randomsize=0.1, randomrot=0.1, maxconnections=2, bunchiness=1.0):
291
292     if (parent is None) or (objectname is None) or (objectname == 'None'):
293         return
294
295     # not necessary, we parent the new objects: p=bpy.context.scene.cursor_location
296
297     theobject = bpy.data.objects[objectname]
298
299     t = gauss(1.0 / probability, 0.1)
300     bpswithleaves = 0
301     for bp in tree.branchpoints:
302         if bp.connections < maxconnections:
303
304             dv = tree.branchpoints[bp.parent].v - bp.v if bp.parent else Vector((0, 0, 0))
305             dvp = Vector((0, 0, 0))
306
307             bpswithleaves += 1
308             nleavesonbp = 0
309             while t < bpswithleaves:
310                 nleavesonbp += 1
311                 rx = (random() - 0.5) * randomrot * 6.283  # TODO vertical tilt in direction of tropism
312                 ry = (random() - 0.5) * randomrot * 6.283
313                 rot = Euler((rx, ry, random() * 6.283), 'ZXY')
314                 scale = size + (random() - 0.5) * randomsize
315
316                 # add new object and parent it
317                 obj = bpy.data.objects.new(objectname, theobject.data)
318                 obj.location = bp.v + dvp
319                 obj.rotation_mode = 'ZXY'
320                 obj.rotation_euler = rot[:]
321                 obj.scale = [scale, scale, scale]
322                 obj.parent = parent
323                 bpy.context.collection.objects.link(obj)
324                 # this is not the best choice of distribution because we might
325                 # get negative values especially if sigma is large
326                 t += gauss(1.0 / probability, 0.1)
327                 dvp = nleavesonbp * (dv / (probability ** bunchiness))  # TODO add some randomness to the offset
328
329
330 def vertextend(v, dv):
331     n = len(v)
332     v.extend(dv)
333     return tuple(range(n, n + len(dv)))
334
335
336 def vertcopy(loopa, v, p):
337     dv = [v[i] + p for i in loopa]
338     # print(loopa, p, dv)
339     return vertextend(v, dv)
340
341
342 def bend(p0, p1, p2, loopa, loopb, verts):
343     # will extend this with a tri centered at p0
344     # print('bend')
345     return bridgequads(loopa, loopb, verts)
346
347
348 def extend(p0, p1, p2, loopa, verts):
349     # will extend this with a tri centered at p0
350     # print('extend')
351     # print(p0,p1,p2,[verts[i] for i in loopa])
352
353     # both difference point upward, we extend to the second
354     d1 = p1 - p0
355     d2 = p0 - p2
356     p = (verts[loopa[0]] + verts[loopa[1]] + verts[loopa[2]] + verts[loopa[3]]) / 4
357     a = d1.angle(d2, 0)
358     if abs(a) < 0.05:
359         # print('small angle')
360         loopb = vertcopy(loopa, verts, p0 - d2 / 2 - p)
361         # all verts in loopb are displaced the same amount so no need to find the minimum distance
362         n = 4
363         return ([(loopa[(i) % n], loopa[(i + 1) % n],
364                  loopb[(i + 1) % n], loopb[(i) % n]) for i in range(n)], loopa, loopb)
365
366     r = d2.cross(d1)
367     q = Quaternion(r, -a)
368     dverts = [verts[i] - p for i in loopa]
369     # print('large angle',dverts,'axis',r)
370     for dv in dverts:
371         dv.rotate(q)
372     # print('rotated',dverts)
373     for dv in dverts:
374         dv += (p0 - d2 / 2)
375     # print('moved',dverts)
376     loopb = vertextend(verts, dverts)
377     # none of the verts in loopb are rotated so no need to find the minimum distance
378     n = 4
379     return ([(loopa[(i) % n], loopa[(i + 1) % n], loopb[(i + 1) % n], loopb[(i) % n]) for i in range(n)], loopa, loopb)
380
381
382 def nonfork(bp, parent, apex, verts, p, branchpoints):
383     # print('nonfork bp    ',bp.index,bp.v,bp.loop if hasattr(bp,'loop') else None)
384     # print('nonfork parent',parent.index,parent.v,parent.loop if hasattr(parent,'loop') else None)
385     # print('nonfork apex  ',apex.index,apex.v,apex.loop if hasattr(apex,'loop') else None)
386     if hasattr(bp, 'loop'):
387         if hasattr(apex, 'loop'):
388             # print('nonfork bend bp->apex')
389             return bend(bp.v + p, parent.v + p, apex.v + p, bp.loop, apex.loop, verts)
390         else:
391             # print('nonfork extend bp->apex')
392             faces, loop1, loop2 = extend(bp.v + p, parent.v + p, apex.v + p, bp.loop, verts)
393             apex.loop = loop2
394             return faces, loop1, loop2
395     else:
396         if hasattr(parent, 'loop'):
397             # print('nonfork extend from bp->parent')
398             # faces,loop1,loop2 =  extend(bp.v+p, apex.v+p, parent.v+p, parent.loop, verts)
399             if parent.parent is None:
400                 return None, None, None
401             grandparent = branchpoints[parent.parent]
402             faces, loop1, loop2 = extend(grandparent.v + p, parent.v + p, bp.v + p, parent.loop, verts)
403             bp.loop = loop2
404             return faces, loop1, loop2
405         else:
406             # print('nonfork no loop')
407             # neither parent nor apex already have a loop calculated
408             # will fill this later ...
409             return None, None, None
410
411
412 def endpoint(bp, parent, verts, p):
413     # extrapolate to tip of branch. we do not close the tip for now
414     faces, loop1, loop2 = extend(bp.v + p, parent.v + p, bp.v + (bp.v - parent.v) + p, bp.loop, verts)
415     return faces, loop1, loop2
416
417
418 def root(bp, apex, verts, p):
419     # extrapolate non-forked roots
420     faces, loop1, loop2 = extend(bp.v + p, bp.v - (apex.v - bp.v) + p, apex.v + p, bp.loop, verts)
421     apex.loop = loop2
422     return faces, loop1, loop2
423
424
425 def skin(aloop, bloop, faces):
426     n = len(aloop)
427     for i in range(n):
428         faces.append((aloop[i], aloop[(i + 1) % n], bloop[(i + 1) % n], bloop[i]))
429
430
431 def createGeometry(tree, power=0.5, scale=0.01, addleaves=False, pleaf=0.5,
432                    leafsize=0.5, leafrandomsize=0.1, leafrandomrot=0.1,
433                    nomodifiers=True, skinmethod='NATIVE', subsurface=False,
434                    maxleafconnections=2, bleaf=1.0, connectoffset=-0.1,
435                    timeperf=True):
436
437     timings = Timer()
438
439     p = bpy.context.scene.cursor_location
440     verts = []
441     edges = []
442     faces = []
443     radii = []
444     roots = set()
445
446     # Loop over all branchpoints and create connected edges
447     for n, bp in enumerate(tree.branchpoints):
448         verts.append(bp.v + p)
449         radii.append(bp.connections)
450         bp.index = n
451         if not (bp.parent is None):
452             edges.append((len(verts) - 1, bp.parent))
453         else:
454             nv = len(verts)
455             roots.add(nv - 1)
456
457     timings.add('skeleton')
458
459     # native skinning method
460     if nomodifiers is False and skinmethod == 'NATIVE':
461         # add a quad edge loop to all roots
462         for r in roots:
463             rootp = verts[r]
464             nv = len(verts)
465             radius = 0.7071 * ((tree.branchpoints[r].connections + 1) ** power) * scale
466             verts.extend(
467                 [rootp + Vector((-radius, -radius, 0)),
468                  rootp + Vector((radius, -radius, 0)),
469                  rootp + Vector((radius, radius, 0)),
470                  rootp + Vector((-radius, radius, 0))]
471             )
472             tree.branchpoints[r].loop = (nv, nv + 1, nv + 2, nv + 3)
473             # print('root verts',tree.branchpoints[r].loop)
474             # faces.append((nv, nv + 1,nv + 2))
475             edges.extend([(nv, nv + 1), (nv + 1, nv + 2), (nv + 2, nv + 3), (nv + 3, nv)])
476
477         # skin all forked branchpoints, no attempt is yet made to adjust the radius
478         forkfork = set()
479         for bpi, bp in enumerate(tree.branchpoints):
480             if not(bp.apex is None or bp.shoot is None):
481                 apex = tree.branchpoints[bp.apex]
482                 shoot = tree.branchpoints[bp.shoot]
483                 p0 = bp.v
484                 r0 = ((bp.connections + 1) ** power) * scale
485                 p2 = apex.v
486                 r2 = ((apex.connections + 1) ** power) * scale
487                 p3 = shoot.v
488                 r3 = ((shoot.connections + 1) ** power) * scale
489
490                 if bp.parent is not None:
491                     parent = tree.branchpoints[bp.parent]
492                     p1 = parent.v
493                     r1 = (parent.connections ** power) * scale
494                 else:
495                     p1 = p0 - (p2 - p0)
496                     r1 = r0
497
498                 skinverts, skinfaces = quadfork(p0, p1, p2, p3, r0, r1, r2, r3)
499                 nv = len(verts)
500                 verts.extend([v + p for v in skinverts])
501                 faces.extend([tuple(v + nv for v in f) for f in skinfaces])
502
503                 # the vertices of the quads at the end of the internodes
504                 # are returned as the first 12 vertices of a total of 22
505                 # we store them for reuse by non-forked internodes but
506                 # first check if we have a fork to fork connection
507                 nv = len(verts)
508                 if hasattr(bp, 'loop') and not (bpi in forkfork):  # already assigned by another fork
509                     faces.extend(bridgequads(bp.loop, [nv - 22, nv - 21, nv - 20, nv - 19], verts)[0])
510                     forkfork.add(bpi)
511                 else:
512                     bp.loop = [nv - 22, nv - 21, nv - 20, nv - 19]
513                 # already assigned by another fork but not yet skinned
514                 if hasattr(apex, 'loop') and not (bp.apex in forkfork):
515                     faces.extend(bridgequads(apex.loop, [nv - 18, nv - 17, nv - 16, nv - 15], verts)[0])
516                     forkfork.add(bp.apex)
517                 else:
518                     apex.loop = [nv - 18, nv - 17, nv - 16, nv - 15]
519                 # already assigned by another fork but not yet skinned
520                 if hasattr(shoot, 'loop') and not (bp.shoot in forkfork):
521                     faces.extend(bridgequads(shoot.loop, [nv - 14, nv - 13, nv - 12, nv - 11], verts)[0])
522                     forkfork.add(bp.shoot)
523                 else:
524                     shoot.loop = [nv - 14, nv - 13, nv - 12, nv - 11]
525
526         # skin the roots that are not forks
527         for r in roots:
528             bp = tree.branchpoints[r]
529             if bp.apex is not None and bp.parent is None and bp.shoot is None:
530                 bfaces, apexloop, parentloop = root(bp, tree.branchpoints[bp.apex], verts, p)
531                 if bfaces is not None:
532                     faces.extend(bfaces)
533
534         # skin all non-forking branchpoints, that is those not a root or and endpoint
535         skinnednonforks = set()
536         start = -1
537         while(start != len(skinnednonforks)):
538             start = len(skinnednonforks)
539             # print('-' * 20, start)
540             for bp in tree.branchpoints:
541                 if bp.shoot is None and not (bp.parent is None or bp.apex is None or bp in skinnednonforks):
542                     bfaces, apexloop, parentloop = nonfork(
543                                                     bp, tree.branchpoints[bp.parent],
544                                                     tree.branchpoints[bp.apex], verts,
545                                                     p, tree.branchpoints
546                                                     )
547                     if bfaces is not None:
548                         # print(bfaces,apexloop,parentloop)
549                         faces.extend(bfaces)
550                         skinnednonforks.add(bp)
551
552         # skin endpoints
553         for bp in tree.branchpoints:
554             if bp.apex is None and bp.parent is not None:
555                 bfaces, apexloop, parentloop = endpoint(bp, tree.branchpoints[bp.parent], verts, p)
556                 if bfaces is not None:
557                     faces.extend(bfaces)
558     # end of native skinning section
559     timings.add('nativeskin')
560
561     # create the tree mesh
562     mesh = bpy.data.meshes.new('Tree')
563     mesh.from_pydata(verts, edges, faces)
564     mesh.update(calc_edges=True)
565
566     # create the tree object an make it the only selected and active object in the scene
567     obj_new = bpy.data.objects.new(mesh.name, mesh)
568     base = bpy.context.collection.objects.link(obj_new)
569     for ob in bpy.context.scene.objects:
570         ob.select_set(False)
571     base.select_set(True)
572     bpy.context.view_layer.objects.active = obj_new
573     bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
574
575     timings.add('createmesh')
576
577     # add a subsurf modifier to smooth the branches
578     if nomodifiers is False:
579         if subsurface:
580             bpy.ops.object.modifier_add(type='SUBSURF')
581             bpy.context.active_object.modifiers[0].levels = 1
582             bpy.context.active_object.modifiers[0].render_levels = 1
583
584         # add a skin modifier
585         if skinmethod == 'BLENDER':
586             bpy.ops.object.modifier_add(type='SKIN')
587             bpy.context.active_object.modifiers[-1].use_smooth_shade = True
588             bpy.context.active_object.modifiers[-1].use_x_symmetry = True
589             bpy.context.active_object.modifiers[-1].use_y_symmetry = True
590             bpy.context.active_object.modifiers[-1].use_z_symmetry = True
591
592             skinverts = bpy.context.active_object.data.skin_vertices[0].data
593
594             for i, v in enumerate(skinverts):
595                 v.radius = [(radii[i] ** power) * scale, (radii[i] ** power) * scale]
596                 if i in roots:
597                     v.use_root = True
598
599             # add an extra subsurf modifier to smooth the skin
600             bpy.ops.object.modifier_add(type='SUBSURF')
601             bpy.context.active_object.modifiers[-1].levels = 1
602             bpy.context.active_object.modifiers[-1].render_levels = 2
603
604     timings.add('modifiers')
605
606     # create the leaves object
607     if addleaves:
608         mesh = createLeaves(
609                     tree, pleaf, leafsize, leafrandomsize,
610                     leafrandomrot, maxleafconnections,
611                     bleaf, connectoffset
612                 )
613         obj_leaves = bpy.data.objects.new(mesh.name, mesh)
614         base = bpy.context.collection.objects.link(obj_leaves)
615         obj_leaves.parent = obj_new
616         bpy.context.view_layer.objects.active = obj_leaves
617         bpy.ops.object.origin_set(type='ORIGIN_CURSOR')
618         bpy.context.view_layer.objects.active = obj_new
619
620     timings.add('leaves')
621
622     if timeperf:
623         print(timings)
624
625     return obj_new
626
627
628 class SCATree(bpy.types.Operator):
629     bl_idname = "mesh.sca_tree"
630     bl_label = "SCATree"
631     bl_description = "Generate a tree using a space colonization algorithm"
632     bl_options = {'REGISTER', 'UNDO', 'PRESET'}
633
634     internodeLength: FloatProperty(
635         name="Internode Length",
636         description="Internode length in Blender Units",
637         default=0.75,
638         min=0.01,
639         soft_max=3.0,
640         subtype='DISTANCE',
641         unit='LENGTH'
642     )
643     killDistance: FloatProperty(
644         name="Kill Distance",
645         description="Kill Distance as a multiple of the internode length",
646         default=3,
647         min=0.01,
648         soft_max=100.0
649     )
650     influenceRange: FloatProperty(
651         name="Influence Range",
652         description="Influence Range as a multiple of the internode length",
653         default=15,
654         min=0.01,
655         soft_max=100.0
656     )
657     tropism: FloatProperty(
658         name="Tropism",
659         description="The tendency of branches to bend up or down",
660         default=0,
661         min=-1.0,
662         soft_max=1.0
663     )
664     power: FloatProperty(
665         name="Power",
666         description="Tapering power of branch connections",
667         default=0.3,
668         min=0.01,
669         soft_max=1.0
670     )
671     scale: FloatProperty(
672         name="Scale",
673         description="Branch size",
674         default=0.01,
675         min=0.0001,
676         soft_max=1.0
677     )
678     # the group related properties are not saved as presets because on reload
679     # no groups with the same names might exist, causing an exception
680     useGroups: BoolProperty(
681         name="Use object groups",
682         options={'ANIMATABLE', 'SKIP_SAVE'},
683         description="Use groups of objects to specify marker distribution",
684         default=False
685     )
686     crownGroup: EnumProperty(
687         items=availableGroups,
688         options={'ANIMATABLE', 'SKIP_SAVE'},
689         name='Crown Group',
690         description='Group of objects that specify crown shape'
691     )
692     shadowGroup: EnumProperty(
693         items=availableGroupsOrNone,
694         options={'ANIMATABLE', 'SKIP_SAVE'},
695         name='Shadow Group',
696         description='Group of objects subtracted from the crown shape'
697     )
698     exclusionGroup: EnumProperty(
699         items=availableGroupsOrNone,
700         options={'ANIMATABLE', 'SKIP_SAVE'},
701         name='Exclusion Group',
702         description='Group of objects that will not be penetrated by growing branches'
703     )
704     useTrunkGroup: BoolProperty(
705         name="Use trunk group",
706         options={'ANIMATABLE', 'SKIP_SAVE'},
707         description="Use the locations of a group of objects to "
708                     "specify trunk starting points instead of 3d cursor",
709         default=False
710     )
711     trunkGroup: EnumProperty(
712         items=availableGroups,
713         options={'ANIMATABLE', 'SKIP_SAVE'},
714         name='Trunk Group',
715         description='Group of objects whose locations specify trunk starting points'
716     )
717     crownSize: FloatProperty(
718         name="Crown Size",
719         description="Crown size",
720         default=5,
721         min=1,
722         soft_max=29
723     )
724     crownShape: FloatProperty(
725         name="Crown Shape",
726         description="Crown shape",
727         default=1,
728         min=0.2,
729         soft_max=5
730     )
731     crownOffset: FloatProperty(
732         name="Crown Offset",
733         description="Crown offset (the length of the bole)",
734         default=3,
735         min=0,
736         soft_max=20.0
737     )
738     surfaceBias: FloatProperty(
739         name="Surface Bias",
740         description="Surface bias (how much markers are favored near the surface)",
741         default=1,
742         min=-10,
743         soft_max=10
744     )
745     topBias: FloatProperty(
746         name="Top Bias",
747         description="Top bias (how much markers are favored near the top)",
748         default=1,
749         min=-10,
750         soft_max=10
751     )
752     randomSeed: IntProperty(
753         name="Random Seed",
754         description="The seed governing random generation",
755         default=0,
756         min=0
757     )
758     maxIterations: IntProperty(
759         name="Maximum Iterations",
760         description="The maximum number of iterations allowed for tree generation",
761         default=40,
762         min=0
763     )
764     numberOfEndpoints: IntProperty(
765         name="Number of Endpoints",
766         description="The number of endpoints generated in the growing volume",
767         default=100,
768         min=0
769     )
770     newEndPointsPer1000: IntProperty(
771         name="Number of new Endpoints",
772         description="The number of new endpoints generated in the growing volume per thousand iterations",
773         default=0,
774         min=0
775     )
776     maxTime: FloatProperty(
777         name="Maximum Time",
778         description=("The maximum time to run the generation for "
779                     "in seconds/generation (0.0 = Disabled). Currently ignored"),
780         default=0.0,
781         min=0.0,
782         soft_max=10
783     )
784     pLeaf: FloatProperty(
785         name="Leaves per internode",
786         description=("The average number of leaves per internode"),
787         default=0.5,
788         min=0.0,
789         soft_max=4
790     )
791     bLeaf: FloatProperty(
792         name="Leaf clustering",
793         description=("How much leaves cluster to the end of the internode"),
794         default=1,
795         min=1,
796         soft_max=4
797     )
798     leafSize: FloatProperty(
799         name="Leaf Size",
800         description=("The leaf size"),
801         default=0.5,
802         min=0.0,
803         soft_max=1
804     )
805     leafRandomSize: FloatProperty(
806         name="Leaf Random Size",
807         description=("The amount of randomness to add to the leaf size"),
808         default=0.1,
809         min=0.0,
810         soft_max=10
811     )
812     leafRandomRot: FloatProperty(
813         name="Leaf Random Rotation",
814         description=("The amount of random rotation to add to the leaf"),
815         default=0.1,
816         min=0.0,
817         soft_max=1
818     )
819     connectoffset: FloatProperty(
820         name="Connect Offset",
821         description=("Offset of leaf to twig"),
822         default=-0.1
823     )
824     leafMaxConnections: IntProperty(
825         name="Max Connections",
826         description="The maximum number of connections of an internode elegible for a leaf",
827         default=2,
828         min=0
829     )
830     addLeaves: BoolProperty(
831         name="Add Leaves",
832         default=False
833     )
834     objectName: EnumProperty(
835         items=availableObjects,
836         options={'ANIMATABLE', 'SKIP_SAVE'},
837         name='Object Name',
838         description='Name of additional objects to duplicate at the branchpoints'
839     )
840     pObject: FloatProperty(
841         name="Objects per internode",
842         description=("The average number of objects per internode"),
843         default=0.3,
844         min=0.0,
845         soft_max=1
846     )
847     bObject: FloatProperty(
848         name="Object clustering",
849         description=("How much objects cluster to the end of the internode"),
850         default=1,
851         min=1,
852         soft_max=4
853     )
854     objectSize: FloatProperty(
855         name="Object Size",
856         description=("The object size"),
857         default=1,
858         min=0.0,
859         soft_max=2
860     )
861     objectRandomSize: FloatProperty(
862         name="Object Random Size",
863         description=("The amount of randomness to add to the object size"),
864         default=0.1,
865         min=0.0,
866         soft_max=10
867     )
868     objectRandomRot: FloatProperty(
869         name="Object Random Rotation",
870         description=("The amount of random rotation to add to the object"),
871         default=0.1,
872         min=0.0,
873         soft_max=1
874     )
875     objectMaxConnections: IntProperty(
876         name="Max Connections for Object",
877         description="The maximum number of connections of an internode elegible for a object",
878         default=1,
879         min=0
880     )
881     addObjects: BoolProperty(
882         name="Add Objects",
883         default=False
884     )
885     updateTree: BoolProperty(
886         name="Update Tree",
887         default=False
888     )
889     noModifiers: BoolProperty(
890         name="No Modifers",
891         default=True
892     )
893     subSurface: BoolProperty(
894         name="Sub Surface",
895         default=False,
896         description="Add subsurface modifier to trunk skin"
897     )
898     skinMethod: EnumProperty(
899         items=[('NATIVE', 'Native', 'Built in skinning method', 1),
900                ('BLENDER', 'Skin modifier', 'Use Blenders skin modifier', 2)],
901         options={'ANIMATABLE', 'SKIP_SAVE'},
902         name='Skinning method',
903         description='How to add a surface to the trunk skeleton'
904     )
905     showMarkers: BoolProperty(
906         name="Show Markers",
907         default=False
908     )
909     markerScale: FloatProperty(
910         name="Marker Scale",
911         description=("The size of the markers"),
912         default=0.05,
913         min=0.001,
914         soft_max=0.2
915     )
916     timePerformance: BoolProperty(
917         name="Time performance",
918         default=False,
919         description="Show duration of generation steps on console"
920     )
921
922     @classmethod
923     def poll(self, context):
924         # Check if we are in object mode
925         return context.mode == 'OBJECT'
926
927     def execute(self, context):
928
929         if not self.updateTree:
930             return {'PASS_THROUGH'}
931
932         timings = Timer()
933
934         # necessary otherwize ray casts toward these objects may fail.
935         # However if nothing is selected, we get a runtime error ...
936         try:
937             bpy.ops.object.mode_set(mode='EDIT', toggle=False)
938             bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
939         except RuntimeError:
940             pass
941
942         if self.useGroups:
943             size, minp = groupExtends(self.crownGroup)
944             volumefie = partial(
945                             groupdistribution, self.crownGroup, self.shadowGroup,
946                             self.randomSeed, size, minp - bpy.context.scene.cursor_location
947                         )
948         else:
949             volumefie = partial(
950                             ellipsoid2, self.crownSize * self.crownShape, self.crownSize,
951                             Vector((0, 0, self.crownSize + self.crownOffset)),
952                             self.surfaceBias, self.topBias
953                         )
954
955         startingpoints = []
956         if self.useTrunkGroup:
957             if bpy.data.collections.find(self.trunkGroup) >= 0:
958                 for ob in bpy.data.collections[self.trunkGroup].objects:
959                     p = ob.location - context.scene.cursor_location
960                     startingpoints.append(Branchpoint(p, None))
961
962         timings.add('scastart')
963         sca = SCA(NBP=self.maxIterations,
964             NENDPOINTS=self.numberOfEndpoints,
965             d=self.internodeLength,
966             KILLDIST=self.killDistance,
967             INFLUENCE=self.influenceRange,
968             SEED=self.randomSeed,
969             TROPISM=self.tropism,
970             volume=volumefie,
971             exclude=lambda p: insidegroup(p, self.exclusionGroup),
972             startingpoints=startingpoints)
973         timings.add('sca')
974
975         if self.showMarkers:
976             mesh = createMarkers(sca, self.markerScale)
977             obj_markers = bpy.data.objects.new(mesh.name, mesh)
978             base = bpy.context.collection.objects.link(obj_markers)
979         timings.add('showmarkers')
980
981         sca.iterate2(newendpointsper1000=self.newEndPointsPer1000, maxtime=self.maxTime)
982         timings.add('iterate')
983
984         obj_new = createGeometry(
985                     sca, self.power, self.scale, self.addLeaves,
986                     self.pLeaf, self.leafSize, self.leafRandomSize, self.leafRandomRot,
987                     self.noModifiers, self.skinMethod, self.subSurface,
988                     self.leafMaxConnections, self.bLeaf, self.connectoffset,
989                     self.timePerformance
990                 )
991
992         timings.add('objcreationstart')
993         if self.addObjects:
994             createObjects(sca, obj_new,
995                 objectname=self.objectName,
996                 probability=self.pObject,
997                 size=self.objectSize,
998                 randomsize=self.objectRandomSize,
999                 randomrot=self.objectRandomRot,
1000                 maxconnections=self.objectMaxConnections,
1001                 bunchiness=self.bObject)
1002         timings.add('objcreation')
1003
1004         if self.showMarkers:
1005             obj_markers.parent = obj_new
1006
1007         self.updateTree = False
1008
1009         if self.timePerformance:
1010             timings.add('Total')
1011             print(timings)
1012
1013         return {'FINISHED'}
1014
1015     def draw(self, context):
1016         layout = self.layout
1017
1018         layout.prop(self, 'updateTree', icon='MESH_DATA')
1019
1020         columns = layout.row()
1021         col1 = columns.column()
1022         col2 = columns.column()
1023
1024         box = col1.box()
1025         box.label(text="Generation Settings:")
1026         box.prop(self, 'randomSeed')
1027         box.prop(self, 'maxIterations')
1028
1029         box = col1.box()
1030         box.label(text="Shape Settings:")
1031         box.prop(self, 'numberOfEndpoints')
1032         box.prop(self, 'internodeLength')
1033         box.prop(self, 'influenceRange')
1034         box.prop(self, 'killDistance')
1035         box.prop(self, 'power')
1036         box.prop(self, 'scale')
1037         box.prop(self, 'tropism')
1038
1039         newbox = col2.box()
1040         newbox.label(text="Crown shape")
1041         newbox.prop(self, 'useGroups')
1042         if self.useGroups:
1043             newbox.label(text="Object groups defining crown shape")
1044             groupbox = newbox.box()
1045             groupbox.prop(self, 'crownGroup')
1046             groupbox = newbox.box()
1047             groupbox.alert = (self.shadowGroup == self.crownGroup)
1048             groupbox.prop(self, 'shadowGroup')
1049             groupbox = newbox.box()
1050             groupbox.alert = (self.exclusionGroup == self.crownGroup)
1051             groupbox.prop(self, 'exclusionGroup')
1052         else:
1053             newbox.label(text="Simple ellipsoid defining crown shape")
1054             newbox.prop(self, 'crownSize')
1055             newbox.prop(self, 'crownShape')
1056             newbox.prop(self, 'crownOffset')
1057         newbox = col2.box()
1058         newbox.prop(self, 'useTrunkGroup')
1059         if self.useTrunkGroup:
1060             newbox.prop(self, 'trunkGroup')
1061
1062         box.prop(self, 'surfaceBias')
1063         box.prop(self, 'topBias')
1064         box.prop(self, 'newEndPointsPer1000')
1065
1066         box = col2.box()
1067         box.label(text="Skin options:")
1068         box.prop(self, 'noModifiers')
1069         if not self.noModifiers:
1070             box.prop(self, 'skinMethod')
1071             box.prop(self, 'subSurface')
1072
1073         layout.prop(self, 'addLeaves', icon='MESH_DATA')
1074         if self.addLeaves:
1075             box = layout.box()
1076             box.label(text="Leaf Settings:")
1077             box.prop(self, 'pLeaf')
1078             box.prop(self, 'bLeaf')
1079             box.prop(self, 'leafSize')
1080             box.prop(self, 'leafRandomSize')
1081             box.prop(self, 'leafRandomRot')
1082             box.prop(self, 'connectoffset')
1083             box.prop(self, 'leafMaxConnections')
1084
1085         layout.prop(self, 'addObjects', icon='MESH_DATA')
1086         if self.addObjects:
1087             box = layout.box()
1088             box.label(text="Object Settings:")
1089             box.prop(self, 'objectName')
1090             box.prop(self, 'pObject')
1091             box.prop(self, 'bObject')
1092             box.prop(self, 'objectSize')
1093             box.prop(self, 'objectRandomSize')
1094             box.prop(self, 'objectRandomRot')
1095             box.prop(self, 'objectMaxConnections')
1096
1097         box = layout.box()
1098         box.label(text="Debug Settings:")
1099         box.prop(self, 'showMarkers')
1100         if self.showMarkers:
1101             box.prop(self, 'markerScale')
1102         box.prop(self, 'timePerformance')
1103
1104
1105 def menu_func(self, context):
1106     self.layout.operator(
1107         SCATree.bl_idname, text="Add Tree to Scene",
1108         icon='PLUGIN').updateTree = True
1109
1110
1111 def register():
1112     bpy.utils.register_module(__name__)
1113     bpy.types.VIEW3D_MT_mesh_add.append(menu_func)
1114
1115
1116 def unregister():
1117     bpy.types.VIEW3D_MT_mesh_add.remove(menu_func)
1118     bpy.utils.unregister_module(__name__)
1119
1120
1121 if __name__ == "__main__":
1122     register()