-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtree_gen.py
932 lines (697 loc) · 35 KB
/
tree_gen.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
# Filename: tree_gen.py
# Author: Stefan Manolache
# This script is used to generate a semi-realistic tree model, together with an
# animation of its growth over its lifespan (including leaf growth and decay,
# flower blooming).
TIME_INTERVAL = 20 # how often to update simulation
import math
import random
import bpy
import numpy
import time
from mathutils import Vector, Matrix, Euler
from typing import NamedTuple, List, Tuple
# Time
#---------------------------------------------------------------------------------
startTime = time.time()
def clock():
print(time.time() - startTime)
# Random stuff :)
#---------------------------------------------------------------------------------
# Represents a random uniformly distributed variable
class RandomValue(NamedTuple):
mean: float
error: float
# Sample the RV
def get(self):
x = numpy.random.uniform(self.mean - self.error,
self.mean + self.error,
None)
return x
RV = RandomValue
# Represents a mesh which can vary in appearance
class RandomMesh:
def __init__(self, meshNames):
# All possible meshes
self.meshes = []
for name in meshNames:
self.meshes.append(bpy.data.objects[name])
# Get deterministic mesh
def get(self):
# Choose a mesh uniformly at random
srcMesh = random.choice(self.meshes)
#Duplicate the source mesh
mesh = srcMesh.copy()
# Important, so that mesh data is not linked to the original
mesh.data = initMesh.data.copy()
#Add the mesh to the collection
bpy.context.collection.objects.link(mesh)
return mesh
RM = RandomMesh
# Blender utility
#---------------------------------------------------------------------------------
# Doing ops inside the script has a lot of overhead, so instead of
# creating the bone when the function is called, we add the task to a queue
# and we create all the bones at the end of the run.
editBonesQ = []
keyframesQ = []
# Add an edit bone to the queue
def createBone(id, headLoc, tailLoc, parentId = None,
connected = False, inheritScale = 'NONE', inheritRotation = True):
name = "bone_" + str(id)
if parentId == None:
parentName = None
else:
parentName = "bone_" + str(parentId)
editBonesQ.append((name, headLoc, tailLoc,
parentName, connected, inheritScale, inheritRotation))
return name
# Flush the edit bones queue, adding the bones to the rig
def flushEditBonesQ(rig):
# Select the armature
bpy.context.view_layer.objects.active = rig
# Need to go into edit mode to add the bones
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
# Add the bones to the rig
for task in editBonesQ:
(name, headLoc, tailLoc, parentName, connected,
inheritScale, inheritRotation) = task
makeEditBone(rig, name, headLoc, tailLoc,
parentName, connected, inheritScale, inheritRotation)
# Go back into object mode
bpy.ops.object.mode_set(mode='OBJECT')
# Add an individual edit bone to the rig
def makeEditBone(rig, name, headLoc, tailLoc, parentName = None,
connected = False, inheritScale = 'NONE', inheritRotation = True):
# Make sure we are in edit mode
assert bpy.context.object.mode == 'EDIT'
# Add a bone to the armature
bone = rig.data.edit_bones.new(name)
bone.head = headLoc
bone.tail = tailLoc
# Set parent if applicable
if parentName is not None:
parent = rig.data.edit_bones[parentName]
bone.parent = parent
# Set properties
bone.use_connect = connected
bone.inherit_scale = inheritScale
bone.use_inherit_rotation = inheritRotation
# Add the keyframe to the queue
def addKeyframe(boneName, type, transform, frame, relative = False):
keyframesQ.append((boneName, type, transform, frame, relative))
# Flush the keyframes and add them to the rig
def flushKeyframesQ(rig):
for task in keyframesQ:
(boneName, type, transform, frame, relative) = task
makeKeyframe(rig, boneName, type, transform, frame, relative)
# Add the keyframe to the specific bone in the rig
def makeKeyframe(rig, boneName, type, transform, frame, relative):
bone = rig.pose.bones[boneName]
if type == 'scale':
if not relative:
bone.scale = (0.0,0.0,0.0)
bone.scale = (Vector(bone.scale) + Vector(transform)).to_tuple()
bone.keyframe_insert('scale', frame = frame)
elif type == 'location':
if not relative:
bone.location = (0.0,0.0,0.0)
bone.location = (Vector(bone.location) + Vector(transform)).to_tuple()
bone.keyframe_insert('location', frame = frame)
# Create a vertex group that assigns full weights to the bone with the given name
def createVertexGroup(name, object):
#assign vertex group weight
group = object.vertex_groups.new(name = name)
verts = []
for v in object.data.vertices:
verts.append(v.index)
group.add(verts, 1.0, 'ADD')
# Growth Functions
#---------------------------------------------------------------------------------
# The growth function describes the development of different aspects of the tree
# (such as primary/secondary growth, flower growth, etc.)
# It is represented by 2 fcurves (blender objects acting as 1 dimensional functions)
# The day curve describes the growth over a year
# The year curve gives how potent this growth is over a certain year in the tree's lifetime
# The error variable is used to give variance to the evaluated growth
class GrowthFunction:
def __init__(self, fcurveDay, fcurveYear, error):
self.fcurveDay = fcurveDay
self.fcurveYear = fcurveYear
self.error = error
def evaluate(self, year, startDay, endDay):
yearModifier = self.fcurveYear.evaluate(year)
mean = self.fcurveDay.evaluate(endDay) - self.fcurveDay.evaluate(startDay)
if (endDay < startDay): # if end day is in the next year
mean += self.fcurveDay.evaluate(365)
return RV(mean * yearModifier, self.error)
# Tree part templates
# These are used in the growth logic as a blueprint for the production rule
#---------------------------------------------------------------------------------
# Production rule for a stem segment
class StemTemplate(NamedTuple):
meshR: RandomMesh # mesh of the stem
lengthRatioR: RV # length of the stem
# Production rule for a leaf
class LeafTemplate(NamedTuple):
meshR: RandomMesh # mesh of the leaf
sizeR: RV # size of the leaf
# This encodes both a flower and its resulting fruit
class FlowerTemplate(NamedTuple):
flowerMeshR: RandomMesh # mesh of the flower
fruitMeshR: RandomMesh # mesh of the fruit
sizeR: RV # size of the flower/fruit
# Production rule for a bud
# This does not directly include what the bud could grow into
# Instead, that is encoded in the index variable
# see Diagram 1 for angle explanation
class BudTemplate(NamedTuple):
index: int # bud type index
dominance: float # how much apical dominance the bud exhibits
brcAngleR: RV # branching angle
divAngleR: RV # divergence angle
rollAngleR: RV # roll angle
# Bud Growth Logic
#---------------------------------------------------------------------------------
# Production Rules
class ShootGrowthRule(NamedTuple):
stemT: StemTemplate
apiBudT: BudTemplate # apical bud
axilTs: List[Tuple[BudTemplate, LeafTemplate]] # axillary buds and leaves
class FlowerGrowthRule(NamedTuple):
flowerT: FlowerTemplate
# Describes what a specific bud type can grow into
# This class is tightly-coupled with BudCollection
# Instances should only be created by using BudCollection.add
# (not sure how to enforce this -- my python expertise -> 0)
class BudType:
def __init__(self, shootRules, shootWeights, flowerRule):
self.shootRules = shootRules
self.shootWeights = shootWeights # list of probabilities of shoot rules
self.flowerRule = flowerRule
# Apply one of the shoot growth rules (based on the probability distribution)
# Return a stem template and a list of axillary bud templates
def shootGrowth(self):
rule = random.choices(self.shootRules, self.shootWeights)
return rule[0]
def flowerGrowth(self):
return flowerRule
# Stores all possible bud types of a tree
# There should be at least one bud type
# The bud with index 0 is the starting bud
class BudCollection:
def __init__(self):
# dict storing all bud types
self.buds = {}
# add bud type to the collection
def add(self, index, shootRules, shootWeights, flowerRule):
budType = BudType(shootRules, shootWeights, flowerRule)
self.buds[index] = budType
# get the bud type at given index
def get(self, index):
return self.buds[index]
# Tree parts
#---------------------------------------------------------------------------------
# Represents the growing part of the tree, which can either develop into a stem
# (with new nodes and leaves attached) or into a flower.
class Bud:
__slots__ = (
'type', # what kind of bud this is
'worldMatrix', # world transform of the bud
'shootPotential', # how close the bud is to growing into a shoot
'flowerPotential',# how close the bud is to growing into a flower
'stem', # the stem this bud is attached to
'apicalBud', # the parent bud of this bud
'age', # how many shoots the bud produced
'dominance' # the apical dominance exhibited by this bud
)
def __init__(self, tree, budT, parentMatrix, parentStem, apicalBud = None):
# the age becomes 0 after renewing the bud
self.age = -1
# set the apical bud
self.apicalBud = apicalBud
# set the flower potential
self.flowerPotential = 0
self.shootPotential = 1
# apply the template and set the world transformation of the mesh
self.renew(tree, budT, parentMatrix, parentStem)
# Transform the bud after it has sprouted into a shoot
def renew(self, tree, budT, parentMatrix, parentStem):
self.age += 1
# set the type of the bud
self.type = budCollection.get(budT.index)
# set dominance
self.dominance = budT.dominance
# set the parent stem
self.stem = parentStem
# update shoot potential
self.shootPotential -= 1
# set the flower potential
self.flowerPotential = 0
# calculate the world transform of the bud from the budT angles
divAngle = math.radians(budT.divAngleR.get())
brcAngle = math.radians(budT.brcAngleR.get())
rollAngle = math.radians(budT.rollAngleR.get())
eul = Euler((0.0, brcAngle, divAngle), 'XYZ').to_matrix().to_4x4()
roll = Matrix.Rotation(rollAngle, 4, 'Z')
self.worldMatrix = parentMatrix @ eul @ roll
# Update the bud with the given potentials.
def update(self, shootPotential, flowerPotential):
self.shootPotential += shootPotential
self.flowerPotential += flowerPotential
# how much is the growth of this bud inhibited by the apical bud
isInhibited = False
if self.age is 0 and self.apicalBud is not None: #if axilarry bud
distance = (self.apicalBud.worldMatrix.to_translation() - self.worldMatrix.to_translation()).length
if distance / self.apicalBud.dominance < 1:
isInhibited = True
if self.shootPotential > 1:
if not isInhibited:
return self.type.shootGrowth()
else:
self.shootPotential -= 1
if self.flowerPotential > 1 and self.age > 0:
return self.type.flowerGrowth()
return None
# When the bud disappears, it has no influence over the child buds
def done(self):
self.dominance = 0.0001
# Generic tree part encapsulating the mesh and the bone controlling it
class MeshPart:
__slots__ = ('id', 'obj', 'boneName')
def __init__(self, id, randomMesh, rig, parentId,
boneLength = 1, connected = False,
inheritScale = 'NONE', inheritRotation = True,
worldMatrix = Matrix.Identity(4),
scaleMatrix = Matrix.Identity(4)):
self.id = id
#Create mesh
self.obj = randomMesh.get()
self.obj.matrix_world = worldMatrix @ scaleMatrix
self.obj.name = "mesh_" + str(self.id)
#Create bone
headLoc = worldMatrix.to_translation()
tailLoc = (worldMatrix @ Matrix.Translation((0, 0, boneLength))).to_translation()
self.boneName = createBone(self.id, headLoc, tailLoc,
parentId, connected, inheritScale, inheritRotation)
#Create vertex group
createVertexGroup(self.boneName, self.obj)
# Add a keyframe to the bone controlling the mesh
def addKeyframe(self, type, transform, frame, relative = False):
addKeyframe(self.boneName, type, transform, frame, relative)
# A woody segment of the tree between 2 consecutive nodes. Features secondary growth (widening)
class Stem:
__slots__ = ('meshPart', 'length', 'id', 'secondaryGrowthGain', 'thick')
def __init__(self, tree, stemT, parentMatrix, parentLength,
id, parentId, startFrame, endFrame, parentThick):
self.length = parentLength * stemT.lengthRatioR.get()
self.id = id
self.secondaryGrowthGain = 0.0
scaleMatrix = Matrix.Diagonal(Vector((1.0,1.0,self.length,1.0)))
self.meshPart = MeshPart(id, stemT.meshR, tree.rig, parentId,
boneLength = self.length, connected = True,
inheritScale = 'NONE',
worldMatrix = parentMatrix,
scaleMatrix = scaleMatrix)
#animate sprouting (primary growth)
self.thick = parentThick * 0.9
self.meshPart.addKeyframe('scale',(0.0,0.0,0.0), startFrame)
self.meshPart.addKeyframe('scale',(self.thick,0.7,self.thick), endFrame + 20.0*self.length, relative = True)
self.meshPart.addKeyframe('scale',(0.0,0.3,0.0), endFrame + 40.0*self.length, relative = True)
# Apply secondary growth
def update(self, secondaryGrowth, frame):
self.secondaryGrowthGain += secondaryGrowth*0.05 * self.thick
if (self.secondaryGrowthGain > 0.2):
gain = self.secondaryGrowthGain
self.meshPart.addKeyframe('scale',(gain,0.0,gain), frame, relative = True)
self.secondaryGrowthGain = 0.0
# A single leaf
class Leaf:
__slots__ = ('meshPart', 'clorophyll', 'colour')
def __init__(self, tree, leafT, parentMatrix,
id, parentId, startFrame, endFrame):
size = leafT.sizeR.get()
scaleMatrix = Matrix.Diagonal(Vector((size, size, size, 1.0)))
self.meshPart = MeshPart(id, leafT.meshR, tree.rig, parentId,
boneLength = size, connected = False,
inheritScale = 'NONE', inheritRotation = False,
worldMatrix = parentMatrix,
scaleMatrix = scaleMatrix)
self.clorophyll = 1.0
#animate sprouting
self.meshPart.addKeyframe('scale',(0.0,0.0,0.0), startFrame)
self.meshPart.addKeyframe('scale',(0.5,0.5,0.5), endFrame + 20.0*size)
self.meshPart.addKeyframe('scale',(1.0,1.0,1.0), endFrame + 60.0*size)
# Update leaf clorophyll
# Return whether the leaf is still on the tree
def update(self, loss, frame):
self.clorophyll -= loss
if self.clorophyll < 0:
self.fall(frame)
return True
return False
# Make the leaf fall
def fall(self,frame):
rframe = frame + int(numpy.random.uniform(0,10, None))
# Animate the fall
self.meshPart.addKeyframe('location',(0.0,0.0,0.0), rframe, relative = True)
self.meshPart.addKeyframe('location',(0.0,0.1,0.0), rframe+1, relative = True)
self.meshPart.addKeyframe('location',(0.0,0.15,-0.3), rframe + 10, relative = True)
self.meshPart.addKeyframe('location',(0.0,0.1,-0.1), rframe + 31, relative = True)
self.meshPart.addKeyframe('scale',(1.0,1.0,1.0), rframe +30)
self.meshPart.addKeyframe('scale',(0.0,0.0,0.0), rframe +31)
# Flower/Fruit part
class Flower:
__slots__ = ('flowerMeshPart',
'fruitMeshPart',
'potential',
'isFruit')
def __init__(self, tree, flowerT, parentMatrix, flowerId, fruitId, parentId, startFrame, endFrame):
size = flowerT.sizeR.get()
scaleMatrix = Matrix.Diagonal(Vector((size, size, size, 1.0)))
self.flowerMeshPart = MeshPart(flowerId, flowerT.flowerMeshR,
tree.rig, parentId,
boneLength = size, connected = False,
inheritScale = 'NONE',
worldMatrix = parentMatrix,
scaleMatrix = scaleMatrix)
self.fruitMeshPart = MeshPart(fruitId, flowerT.fruitMeshR,
tree.rig, parentId,
boneLength = size, connected = False,
inheritScale = 'NONE',
worldMatrix = parentMatrix,
scaleMatrix = scaleMatrix)
# The potential describes the stage of growth of the flower/fruit part
# 0 < p <= 1: flower; 1 < p <= 2: fruit
self.potential = 0
self.isFruit = False
# Animate sprouting
self.flowerMeshPart.addKeyframe('scale',(0.0,0.0,0.0), startFrame)
self.flowerMeshPart.addKeyframe('scale',(1.0,1.0,1.0), startFrame + 10)
self.fruitMeshPart.addKeyframe('scale',(0.0,0.0,0.0), startFrame)
# Update the potential
def update(self, potential, frame):
self.potential += potential
if not self.isFruit and self.potential > 1:
rframe = frame + int(numpy.random.uniform(0,6, None))
# Animate the flower disappearing
self.flowerMeshPart.addKeyframe('location',(0.0,0.0,0.0), rframe, relative = True)
self.flowerMeshPart.addKeyframe('location',(0.0,-1.0,0.0), rframe + 10, relative = True)
self.flowerMeshPart.addKeyframe('scale',(1.0,1.0,1.0), frame + 9)
self.flowerMeshPart.addKeyframe('scale',(0.0,0.0,0.0), frame + 10)
self.makeFruit(frame)
if self.potential > 2: # isFruit = True
self.fall(frame)
return True
return False
# Animate the fruit appearing
def makeFruit(self,frame):
self.isFruit = True
self.fruitMeshPart.addKeyframe('scale',(0.0,0.0,0.0), frame)
self.fruitMeshPart.addKeyframe('scale',(1.0,1.0,1.0), frame + 15)
# Animate the fruit falling
def fall(self, frame):
rframe = frame + int(numpy.random.uniform(0,10, None))
self.fruitMeshPart.addKeyframe('location',(0.0,0.0,0.0), rframe, relative = True)
self.fruitMeshPart.addKeyframe('location',(0.0,-2.5,0.0), rframe + 20, relative = True)
self.fruitMeshPart.addKeyframe('location',(0.0,-7.0,0.0), rframe + 40, relative = True)
self.fruitMeshPart.addKeyframe('scale',(1.0,1.0,1.0), rframe +40)
self.fruitMeshPart.addKeyframe('scale',(0.0,0.0,0.0), rframe +41)
# Main tree class
#---------------------------------------------------------------------------------
class Tree:
def __init__(self, budCollection, startBudT, primaryGrowthF, secondaryGrowthF,
bloomingF, fruitGrowthF, leafDecayF):
self.budCollection = budCollection
self.createRig()
# age of the tree in years
self.year = 0
# current day in the year
self.day = 1
#id generator
self.idGen = 1
# all tree leaves
self.leaves = []
self.fallenLeaves = []
# all tree flowers
self.flowers = []
self.fallenFlowers = []
# create root stem
stemT = StemTemplate(RandomMesh(['root']), RV(1.0,0.01))
self.root = Stem(self, stemT, Matrix.Translation((0, 0, -1.0)), 1.0,
self.getId(), 0, 0, 0, 1.0)
# all tree stems
self.stems = [self.root]
# create starting bud
startBud = Bud(self, startBudT, Matrix.Identity(4), self.root)
# all tree buds
self.buds = [startBud]
# set growth functions
self.primaryGrowthF = primaryGrowthF
self.secondaryGrowthF = secondaryGrowthF
self.bloomingF = bloomingF
self.fruitGrowthF = fruitGrowthF
self.leafDecayF = leafDecayF
# Tree part id generator
def getId(self):
id = self.idGen
self.idGen += 1
return id
def createRig(self):
# the armature of the tree
armature = bpy.data.armatures.new('TreeArmature')
armature.display_type = 'WIRE'
# create a rig and link it to the collection
self.rig = bpy.data.objects.new('TreeRig', armature)
bpy.context.scene.collection.objects.link(self.rig)
createBone(0, (0.0, 0.0, -1.2), (0.0, 0.0, -1.0))
# Complete the creation of the tree animated model
def complete(self):
# create edit bones
flushEditBonesQ(self.rig)
# create keyframes
flushKeyframesQ(self.rig)
# deselect all objects
bpy.ops.object.select_all(action='DESELECT')
clock()
#select all tree parts
for stem in self.stems:
stem.meshPart.obj.select_set(True)
for leaf in self.leaves + self.fallenLeaves:
leaf.meshPart.obj.select_set(True)
for flower in self.flowers + self.fallenFlowers:
flower.flowerMeshPart.obj.select_set(True)
flower.fruitMeshPart.obj.select_set(True)
clock()
#set branch to be active object
bpy.context.view_layer.objects.active = self.root.meshPart.obj
#join the object together
print("joining...")
bpy.ops.object.join()
treemesh = bpy.context.view_layer.objects.active
# parent the mesh to the armature
treemesh.parent = self.rig
treemesh.name = "TreeMesh"
#add armature modifier
treemesh.modifiers.new(name = 'Armature', type = 'ARMATURE')
treemesh.modifiers['Armature'].object = self.rig
# Simulate the growth of the tree
# time - number of days to simulate growth for
def grow(self, time = 1, leafGrowth = 1.0, flowerGrowth = 1.0):
for i in range(0, time, TIME_INTERVAL):
# update tree age
startDay = self.day
year = self.year
startFrame = self.year * 365 + self.day
self.day += TIME_INTERVAL # TIME_INTERVAL < 365
if self.day > 365:
self.day -= 365
self.year += 1
print("Current age: " + str(self.year))
print("Day: " + str(self.day))
endDay = self.day
endFrame = self.year * 365 + self.day
addedBuds = []
# Update buds
primaryGrowthR = self.primaryGrowthF.evaluate(year, startDay, endDay)
bloomingR = self.bloomingF.evaluate(year, startDay, endDay)
for bud in list(self.buds):
growthRule = bud.update(primaryGrowthR.get(),bloomingR.get())
# Grow into a shoot
if type(growthRule) is ShootGrowthRule:
oldParentMatrix = bud.worldMatrix
stemT = growthRule.stemT
budT = growthRule.apiBudT
axilTs = growthRule.axilTs
parentId = bud.stem.id
# create stem
stem = Stem(self, stemT, oldParentMatrix, bud.stem.length,
self.getId(), parentId, startFrame, endFrame,
bud.stem.thick)
self.stems.append(stem)
stemLength = stem.length
parentMatrix = oldParentMatrix @ Matrix.Translation((0,0,stemLength))
# renew apical bud
bud.renew(self, budT, parentMatrix, stem)
parentId = stem.id
# create axillary buds and leaves
for (axiBudT, leafT) in axilTs:
axiBud = Bud(tree, axiBudT, parentMatrix, stem, bud)
addedBuds.append(axiBud)
if numpy.random.uniform(0.0, 1.0, None) <= leafGrowth:
translation = axiBud.worldMatrix.to_translation()
z = parentMatrix.to_euler('ZXY').z
y = math.radians(RV(90.0,20.0).get())
x = math.radians(RV(0.0,10.0).get())
rotation = Euler((x,y,z),'YZX').to_matrix().to_4x4()
leafWorldMatrix = Matrix.Translation(translation) @ rotation
leaf = Leaf(tree, leafT, leafWorldMatrix,
self.getId(), parentId, startFrame, endFrame)
self.leaves.append(leaf)
# Grow into a flower
elif type(growthRule) is FlowerGrowthRule:
parentMatrix = bud.worldMatrix
flowerT = growthRule.flowerT
parentId = bud.stem.id
# create flower
if numpy.random.uniform(0.0, 1.0, None) <= flowerGrowth:
print("flower")
parentMatrix = bud.worldMatrix
translation = parentMatrix.to_translation()
z = math.radians(RV(0.0,90.0).get())
rotation = Euler((0, 0, z), 'YZX').to_matrix().to_4x4()
flowerWorldMatrix = Matrix.Translation(translation) @ rotation
flower = Flower(self, flowerT, flowerWorldMatrix, self.getId(),
self.getId(), parentId, startFrame, endFrame)
self.flowers.append(flower)
# remove bud
self.buds.remove(bud)
bud.done()
else:
bud.flowerPotential -= 1
self.buds.extend(addedBuds)
# Update stems
secondaryGrowth = self.secondaryGrowthF.evaluate(year, startDay, endDay).get()
for stem in list(self.stems):
stem.update(secondaryGrowth, endFrame)
# Update leaves
leafDecayR = self.leafDecayF.evaluate(year, startDay, endDay)
for leaf in list(self.leaves):
hasFallen = leaf.update(leafDecayR.get(), endFrame)
if hasFallen:
self.leaves.remove(leaf)
self.fallenLeaves.append(leaf)
# Update flowers
fruitGrowthR = self.fruitGrowthF.evaluate(year, startDay, endDay)
for flower in list(self.flowers):
hasFallen = flower.update(fruitGrowthR.get(), endFrame)
if hasFallen:
self.flowers.remove(flower)
self.fallenFlowers.append(flower)
#Templates, rules and growth functions for an Oak
trunkT = StemTemplate(RM(['OakTrunk']),RV(0.8,0.1))
mainT = StemTemplate(RM(['Stem']),RV(0.84,0.1))
branchT = StemTemplate(RM(['Stem']), RV(0.84,0.1))
twigT = StemTemplate(RM(['Stem']), RV(0.84,0.1))
leafT = LeafTemplate(RandomMesh(['OakLeaf0','OakLeaf1']), RV(0.3,0.01))
flowerT = FlowerTemplate(RM(['OakFlower']), RM(['Acorn']), RV(0.3, 0.03))
startBudT = BudTemplate(index = 0, dominance = 8.0,
brcAngleR = RV(0.0,1.0),
divAngleR = RV(0.0,60.0),
rollAngleR = RV(0.0,270.0),
)
budTtrunk = BudTemplate(index = 1, dominance = 2.0,
brcAngleR = RV(0.0,15.0),
divAngleR = RV(0.0,180.0),
rollAngleR = RV(137.0,30.0)
)
budTmainApi = BudTemplate(index = 2, dominance = 2.0,
brcAngleR = RV(10.0,10.0),
divAngleR = RV(0.0,180.0),
rollAngleR = RV(137.0,30.0)
)
budTmain = BudTemplate(index = 2, dominance = 1.0,
brcAngleR = RV(50.0,10.0),
divAngleR = RV(90.0,180.0),
rollAngleR = RV(137.0,30.0)
)
budTbranchApi = BudTemplate(index = 3, dominance = 0.2,
brcAngleR = RV(0.0,10.0),
divAngleR = RV(80.0,180.0),
rollAngleR = RV(137.0,20.0),
)
budTbranch = BudTemplate(index = 3, dominance = 0.1,
brcAngleR = RV(40,10.0),
divAngleR = RV(80.0,180.0),
rollAngleR = RV(137.0,20.0),
)
budTtwigApi = BudTemplate(index = 4, dominance = 0.1,
brcAngleR = RV(4.0,20.0),
divAngleR = RV(180.0,180.0),
rollAngleR = RV(137.0,20.0),
)
budTtwig = BudTemplate(index = 4, dominance = 0.1,
brcAngleR = RV(40.0,20.0),
divAngleR = RV(180.0,180.0),
rollAngleR = RV(137.0,20.0),
)
rule00 = ShootGrowthRule(trunkT, budTtrunk,
[(budTmain, leafT),(budTmain, leafT)])
rule01 = ShootGrowthRule(trunkT, budTtrunk,
[(budTmain, leafT)])
rule02 = ShootGrowthRule(trunkT, budTtrunk,
[])
rule10 = ShootGrowthRule(mainT, budTtrunk,
[(budTmain, leafT),(budTmain, leafT)])
rule11 = ShootGrowthRule(mainT, budTtrunk,
[(budTmain, leafT)])
rule12 = ShootGrowthRule(mainT, budTtrunk,
[])
rule13 = ShootGrowthRule(mainT, budTmainApi,
[(budTmain, leafT)])
rule20 = ShootGrowthRule(branchT, budTmainApi,
[(budTbranch, leafT),(budTbranch, leafT)])
rule21 = ShootGrowthRule(branchT, budTmainApi,
[(budTbranch, leafT)])
rule22 = ShootGrowthRule(branchT, budTmainApi,
[])
rule23 = ShootGrowthRule(branchT, budTbranchApi,
[])
rule30 = ShootGrowthRule(branchT, budTbranchApi,
[(budTtwig, leafT),(budTtwig, leafT)])
rule31 = ShootGrowthRule(branchT, budTbranchApi,
[(budTtwig, leafT)])
rule32 = ShootGrowthRule(branchT, budTtwigApi,
[(budTtwig, leafT)])
rule40 = ShootGrowthRule(twigT, budTtwigApi,
[(budTtwig, leafT),(budTtwig, leafT)])
rule41 = ShootGrowthRule(twigT, budTtwigApi,
[(budTtwig, leafT)])
rule42 = ShootGrowthRule(twigT, budTtwigApi,
[])
flowerRule = FlowerGrowthRule(flowerT)
budCollection = BudCollection()
budCollection.add(0, [rule00, rule01, rule02], [0.8,0.1, 0.1], flowerRule)
budCollection.add(1, [rule10,rule11, rule12, rule13], [0.1,0.3,0.1,0.3], flowerRule)
budCollection.add(2, [rule20,rule21, rule22, rule23], [0.1,0.5,0.1,0.3], flowerRule)
budCollection.add(3, [rule30, rule31, rule32], [0.07,0.5,0.43], flowerRule)
budCollection.add(4, [rule40, rule41,rule42], [0.15, 0.84, 0.01], flowerRule)
fcurves = bpy.data.objects['OakGrowthFunctions'].animation_data.action.fcurves
primaryGrowthF = GrowthFunction(fcurves.find('["primaryGrowthDay"]'),
fcurves.find('["primaryGrowthYear"]'), 0.01)
secondaryGrowthF = GrowthFunction(fcurves.find('["secondaryGrowthDay"]'),
fcurves.find('["secondaryGrowthYear"]'), 0.1)
bloomingF = GrowthFunction(fcurves.find('["bloomingDay"]'),
fcurves.find('["bloomingYear"]'), 0.1)
fruitGrowthF = GrowthFunction(fcurves.find('["fruitGrowthDay"]'),
fcurves.find('["fruitGrowthYear"]'), 0.05)
leafDecayF = GrowthFunction(fcurves.find('["leafDecayDay"]'),
fcurves.find('["leafDecayYear"]'), 0.01)
tree = Tree(budCollection, startBudT,
primaryGrowthF, secondaryGrowthF, bloomingF, fruitGrowthF, leafDecayF)
tree.grow(4380, 1.0, 1.0)
tree.grow(1410, 1.0, 0.4)
print("growing done")
clock()
tree.complete()
clock()