1 # SPDX-License-Identifier: GPL-2.0-or-later
5 "author": "testscreenings, PKHG, TrumanBlending",
8 "location": "View3D > Sidebar > Ivy Generator (Create Tab)",
9 "description": "Adds generated ivy to a mesh object starting "
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/ivy_gen.html",
13 "category": "Add Curve",
18 from bpy
.types
import (
23 from bpy
.props
import (
29 from mathutils
.bvhtree
import BVHTree
30 from mathutils
import (
34 from collections
import deque
46 def createIvyGeometry(IVY
, growLeaves
):
47 """Create the curve geometry for IVY"""
48 # Compute the local size and the gauss weight filter
49 # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
50 gaussWeight
= (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
52 # Create a new curve and initialise it
53 curve
= bpy
.data
.curves
.new("IVY", type='CURVE')
54 curve
.dimensions
= '3D'
56 curve
.fill_mode
= 'FULL'
57 curve
.resolution_u
= 4
60 # Create the ivy leaves
61 # Order location of the vertices
62 signList
= ((-1.0, +1.0),
69 # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
71 # Initialise the vertex and face lists
74 # Store the methods for faster calling
75 addV
= vertList
.extend
76 rotMat
= Matrix
.Rotation
78 # Loop over all roots to generate its nodes
79 for root
in IVY
.ivyRoots
:
80 # Only grow if more than one node
81 numNodes
= len(root
.ivyNodes
)
83 # Calculate the local radius
84 local_ivyBranchRadius
= 1.0 / (root
.parents
+ 1) + 1.0
85 prevIvyLength
= 1.0 / root
.ivyNodes
[-1].length
86 splineVerts
= [ax
for n
in root
.ivyNodes
for ax
in n
.pos
.to_4d()]
88 radiusConstant
= local_ivyBranchRadius
* IVY
.ivyBranchSize
89 splineRadii
= [radiusConstant
* (1.3 - n
.length
* prevIvyLength
)
90 for n
in root
.ivyNodes
]
92 # Add the poly curve and set coords and radii
93 newSpline
= curve
.splines
.new(type='POLY')
94 newSpline
.points
.add(len(splineVerts
) // 4 - 1)
95 newSpline
.points
.foreach_set('co', splineVerts
)
96 newSpline
.points
.foreach_set('radius', splineRadii
)
98 # Loop over all nodes in the root
99 for i
, n
in enumerate(root
.ivyNodes
):
100 for k
in range(len(gaussWeight
)):
101 idx
= max(0, min(i
+ k
- 5, numNodes
- 1))
102 n
.smoothAdhesionVector
+= (gaussWeight
[k
] *
103 root
.ivyNodes
[idx
].adhesionVector
)
104 n
.smoothAdhesionVector
/= 56.0
105 n
.adhesionLength
= n
.smoothAdhesionVector
.length
106 n
.smoothAdhesionVector
.normalize()
108 if growLeaves
and (i
< numNodes
- 1):
109 node
= root
.ivyNodes
[i
]
110 nodeNext
= root
.ivyNodes
[i
+ 1]
112 # Find the weight and normalize the smooth adhesion vector
113 weight
= pow(node
.length
* prevIvyLength
, 0.7)
115 # Calculate the ground ivy and the new weight
116 groundIvy
= max(0.0, -node
.smoothAdhesionVector
.z
)
117 weight
+= groundIvy
* pow(1 - node
.length
*
120 # Find the alignment weight
121 alignmentWeight
= node
.adhesionLength
123 # Calculate the needed angles
124 phi
= atan2(node
.smoothAdhesionVector
.y
,
125 node
.smoothAdhesionVector
.x
) - pi
/ 2.0
128 node
.smoothAdhesionVector
.angle(Vector((0, 0, -1)), 0))
130 # Find the size weight
131 sizeWeight
= 1.5 - (cos(2 * pi
* weight
) * 0.5 + 0.5)
133 # Randomise the angles
134 phi
+= (rand_val() - 0.5) * (1.3 - alignmentWeight
)
135 theta
+= (rand_val() - 0.5) * (1.1 - alignmentWeight
)
137 # Calculate the leaf size an append the face to the list
138 leafSize
= IVY
.ivyLeafSize
* sizeWeight
141 # Generate the probability
142 probability
= rand_val()
144 # If we need to grow a leaf, do so
145 if (probability
* weight
) > IVY
.leafProbability
:
147 # Generate the random vector
148 randomVector
= Vector((rand_val() - 0.5,
153 # Find the leaf center
154 center
= (node
.pos
.lerp(nodeNext
.pos
, j
/ 10.0) +
155 IVY
.ivyLeafSize
* randomVector
)
157 # For each of the verts, rotate/scale and append
158 basisVecX
= Vector((1, 0, 0))
159 basisVecY
= Vector((0, 1, 0))
161 horiRot
= rotMat(theta
, 3, 'X')
162 vertRot
= rotMat(phi
, 3, 'Z')
164 basisVecX
.rotate(horiRot
)
165 basisVecY
.rotate(horiRot
)
167 basisVecX
.rotate(vertRot
)
168 basisVecY
.rotate(vertRot
)
170 basisVecX
*= leafSize
171 basisVecY
*= leafSize
173 addV([k1
* basisVecX
+ k2
* basisVecY
+ center
for
176 # Add the object and link to scene
177 newCurve
= bpy
.data
.objects
.new("IVY_Curve", curve
)
178 bpy
.context
.collection
.objects
.link(newCurve
)
181 faceList
= [[4 * i
+ l
for l
in range(4)] for i
in
182 range(len(vertList
) // 4)]
184 # Generate the new leaf mesh and link
185 me
= bpy
.data
.meshes
.new('IvyLeaf')
186 me
.from_pydata(vertList
, [], faceList
)
187 me
.update(calc_edges
=True)
188 ob
= bpy
.data
.objects
.new('IvyLeaf', me
)
189 bpy
.context
.collection
.objects
.link(ob
)
191 me
.uv_layers
.new(name
="Leaves")
193 # Set the uv texture coords
194 # TODO, this is non-functional, default uvs are ok?
197 uv1, uv2, uv3, uv4 = signList
204 """ The basic class used for each point on the ivy which is grown."""
205 __slots__
= ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
206 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
209 self
.pos
= Vector((0, 0, 0))
210 self
.primaryDir
= Vector((0, 0, 1))
211 self
.adhesionVector
= Vector((0, 0, 0))
212 self
.smoothAdhesionVector
= Vector((0, 0, 0))
214 self
.floatingLength
= 0.0
219 """ The class used to hold all ivy nodes growing from this root point."""
220 __slots__
= ('ivyNodes', 'alive', 'parents')
223 self
.ivyNodes
= deque()
229 """ The class holding all parameters and ivy roots."""
230 __slots__
= ('ivyRoots', 'primaryWeight', 'randomWeight',
231 'gravityWeight', 'adhesionWeight', 'branchingProbability',
232 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
233 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
240 branchingProbability
=0.05,
241 leafProbability
=0.35,
246 maxAdhesionDistance
=1.0):
248 self
.ivyRoots
= deque()
249 self
.primaryWeight
= primaryWeight
250 self
.randomWeight
= randomWeight
251 self
.gravityWeight
= gravityWeight
252 self
.adhesionWeight
= adhesionWeight
253 self
.branchingProbability
= 1 - branchingProbability
254 self
.leafProbability
= 1 - leafProbability
255 self
.ivySize
= ivySize
256 self
.ivyLeafSize
= ivyLeafSize
257 self
.ivyBranchSize
= ivyBranchSize
258 self
.maxFloatLength
= maxFloatLength
259 self
.maxAdhesionDistance
= maxAdhesionDistance
262 # Normalize all the weights only on initialisation
263 sums
= self
.primaryWeight
+ self
.randomWeight
+ self
.adhesionWeight
264 self
.primaryWeight
/= sums
265 self
.randomWeight
/= sums
266 self
.adhesionWeight
/= sums
268 def seed(self
, seedPos
):
269 # Seed the Ivy by making a new root and first node
274 tmpRoot
.ivyNodes
.append(tmpIvy
)
275 self
.ivyRoots
.append(tmpRoot
)
277 def grow(self
, ob
, bvhtree
):
278 # Determine the local sizes
279 # local_ivySize = self.ivySize # * radius
280 # local_maxFloatLength = self.maxFloatLength # * radius
281 # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
283 for root
in self
.ivyRoots
:
284 # Make sure the root is alive, if not, skip
288 # Get the last node in the current root
289 prevIvy
= root
.ivyNodes
[-1]
291 # If the node is floating for too long, kill the root
292 if prevIvy
.floatingLength
> self
.maxFloatLength
:
295 # Set the primary direction from the last node
296 primaryVector
= prevIvy
.primaryDir
298 # Make the random vector and normalize
299 randomVector
= Vector((rand_val() - 0.5, rand_val() - 0.5,
300 rand_val() - 0.5)) + Vector((0, 0, 0.2))
301 randomVector
.normalize()
303 # Calculate the adhesion vector
304 adhesionVector
= adhesion(
305 prevIvy
.pos
, bvhtree
, self
.maxAdhesionDistance
)
307 # Calculate the growing vector
308 growVector
= self
.ivySize
* (primaryVector
* self
.primaryWeight
+
309 randomVector
* self
.randomWeight
+
310 adhesionVector
* self
.adhesionWeight
)
312 # Find the gravity vector
313 gravityVector
= (self
.ivySize
* self
.gravityWeight
*
315 gravityVector
*= pow(prevIvy
.floatingLength
/ self
.maxFloatLength
,
318 # Determine the new position vector
319 newPos
= prevIvy
.pos
+ growVector
+ gravityVector
321 # Check for collisions with the object
322 climbing
, newPos
= collision(bvhtree
, prevIvy
.pos
, newPos
)
324 # Update the growing vector for any collisions
325 growVector
= newPos
- prevIvy
.pos
- gravityVector
326 growVector
.normalize()
328 # Create a new IvyNode and set its properties
330 tmpNode
.climb
= climbing
332 tmpNode
.primaryDir
= prevIvy
.primaryDir
.lerp(growVector
, 0.5)
333 tmpNode
.primaryDir
.normalize()
334 tmpNode
.adhesionVector
= adhesionVector
335 tmpNode
.length
= prevIvy
.length
+ (newPos
- prevIvy
.pos
).length
337 if tmpNode
.length
> self
.maxLength
:
338 self
.maxLength
= tmpNode
.length
340 # If the node isn't climbing, update it's floating length
341 # Otherwise set it to 0
343 tmpNode
.floatingLength
= prevIvy
.floatingLength
+ (newPos
-
346 tmpNode
.floatingLength
= 0.0
348 root
.ivyNodes
.append(tmpNode
)
350 # Loop through all roots to check if a new root is generated
351 for root
in self
.ivyRoots
:
352 # Check the root is alive and isn't at high level of recursion
353 if (root
.parents
> 3) or (not root
.alive
):
356 # Check to make sure there's more than 1 node
357 if len(root
.ivyNodes
) > 1:
358 # Loop through all nodes in root to check if new root is grown
359 for node
in root
.ivyNodes
:
360 # Set the last node of the root and find the weighting
361 prevIvy
= root
.ivyNodes
[-1]
362 weight
= 1.0 - (cos(2.0 * pi
* node
.length
/
363 prevIvy
.length
) * 0.5 + 0.5)
365 probability
= rand_val()
367 # Check if a new root is grown and if so, set its values
368 if (probability
* weight
> self
.branchingProbability
):
370 tmpNode
.pos
= node
.pos
371 tmpNode
.floatingLength
= node
.floatingLength
374 tmpRoot
.parents
= root
.parents
+ 1
376 tmpRoot
.ivyNodes
.append(tmpNode
)
377 self
.ivyRoots
.append(tmpRoot
)
381 def adhesion(loc
, bvhtree
, max_l
):
382 # Compute the adhesion vector by finding the nearest point
383 nearest_location
, *_
= bvhtree
.find_nearest(loc
, max_l
)
384 adhesion_vector
= Vector((0.0, 0.0, 0.0))
385 if nearest_location
is not None:
386 # Compute the distance to the nearest point
387 adhesion_vector
= nearest_location
- loc
388 distance
= adhesion_vector
.length
389 # If it's less than the maximum allowed and not 0, continue
391 # Compute the direction vector between the closest point and loc
392 adhesion_vector
.normalize()
393 adhesion_vector
*= 1.0 - distance
/ max_l
394 # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
395 return adhesion_vector
398 def collision(bvhtree
, pos
, new_pos
):
399 # Check for collision with the object
402 corrected_new_pos
= new_pos
403 direction
= new_pos
- pos
405 hit_location
, hit_normal
, *_
= bvhtree
.ray_cast(pos
, direction
, direction
.length
)
406 # If there's a collision we need to check it
407 if hit_location
is not None:
408 # Check whether the collision is going into the object
409 if direction
.dot(hit_normal
) < 0.0:
410 reflected_direction
= (new_pos
- hit_location
).reflect(hit_normal
)
412 corrected_new_pos
= hit_location
+ reflected_direction
415 return climbing
, corrected_new_pos
418 def bvhtree_from_object(ob
):
422 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
423 ob_eval
= ob
.evaluated_get(depsgraph
)
424 mesh
= ob_eval
.to_mesh()
426 bm
.transform(ob
.matrix_world
)
428 bvhtree
= BVHTree
.FromBMesh(bm
)
429 ob_eval
.to_mesh_clear()
432 def check_mesh_faces(ob
):
434 if len(me
.polygons
) > 0:
440 class IvyGen(Operator
):
441 bl_idname
= "curve.ivy_gen"
443 bl_description
= "Generate Ivy on an Mesh Object"
444 bl_options
= {'REGISTER', 'UNDO'}
446 updateIvy
: BoolProperty(
448 description
="Update the Ivy location based on the cursor and Panel settings",
451 defaultIvy
: BoolProperty(
453 options
={"HIDDEN", "SKIP_SAVE"},
458 def poll(self
, context
):
459 # Check if there's an object and whether it's a mesh
460 ob
= context
.active_object
461 return ((ob
is not None) and
462 (ob
.type == 'MESH') and
463 (context
.mode
== 'OBJECT'))
465 def invoke(self
, context
, event
):
466 self
.updateIvy
= True
467 return self
.execute(context
)
469 def execute(self
, context
):
470 # scene = context.scene
471 ivyProps
= context
.window_manager
.ivy_gen_props
473 if not self
.updateIvy
:
474 return {'PASS_THROUGH'}
476 # assign the variables, check if it is default
477 # Note: update the values if window_manager props defaults are changed
478 randomSeed
= ivyProps
.randomSeed
if not self
.defaultIvy
else 0
479 maxTime
= ivyProps
.maxTime
if not self
.defaultIvy
else 0
480 maxIvyLength
= ivyProps
.maxIvyLength
if not self
.defaultIvy
else 1.0
481 ivySize
= ivyProps
.ivySize
if not self
.defaultIvy
else 0.02
482 maxFloatLength
= ivyProps
.maxFloatLength
if not self
.defaultIvy
else 0.5
483 maxAdhesionDistance
= ivyProps
.maxAdhesionDistance
if not self
.defaultIvy
else 1.0
484 primaryWeight
= ivyProps
.primaryWeight
if not self
.defaultIvy
else 0.5
485 randomWeight
= ivyProps
.randomWeight
if not self
.defaultIvy
else 0.2
486 gravityWeight
= ivyProps
.gravityWeight
if not self
.defaultIvy
else 1.0
487 adhesionWeight
= ivyProps
.adhesionWeight
if not self
.defaultIvy
else 0.1
488 branchingProbability
= ivyProps
.branchingProbability
if not self
.defaultIvy
else 0.05
489 leafProbability
= ivyProps
.leafProbability
if not self
.defaultIvy
else 0.35
490 ivyBranchSize
= ivyProps
.ivyBranchSize
if not self
.defaultIvy
else 0.001
491 ivyLeafSize
= ivyProps
.ivyLeafSize
if not self
.defaultIvy
else 0.02
492 growLeaves
= ivyProps
.growLeaves
if not self
.defaultIvy
else True
494 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
495 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
497 # Get the selected object
498 ob
= context
.active_object
499 bvhtree
= bvhtree_from_object(ob
)
501 # Check if the mesh has at least one polygon since some functions
502 # are expecting them in the object's data (see T51753)
503 check_face
= check_mesh_faces(ob
)
504 if check_face
is False:
505 self
.report({'WARNING'},
506 "Mesh Object doesn't have at least one Face. "
507 "Operation Cancelled")
510 # Compute bounding sphere radius
511 # radius = computeBoundingSphere(ob) # Not needed anymore
513 # Get the seeding point
514 seedPoint
= context
.scene
.cursor
.location
516 # Fix the random seed
517 rand_seed(randomSeed
)
521 primaryWeight
=primaryWeight
,
522 randomWeight
=randomWeight
,
523 gravityWeight
=gravityWeight
,
524 adhesionWeight
=adhesionWeight
,
525 branchingProbability
=branchingProbability
,
526 leafProbability
=leafProbability
,
528 ivyLeafSize
=ivyLeafSize
,
529 ivyBranchSize
=ivyBranchSize
,
530 maxFloatLength
=maxFloatLength
,
531 maxAdhesionDistance
=maxAdhesionDistance
533 # Generate first root and node
537 maxLength
= maxIvyLength
# * radius
539 # If we need to check time set the flag
545 checkAliveIter
= [True, ]
547 # Grow until 200 roots is reached or backup counter exceeds limit
548 while (any(checkAliveIter
) and
549 (IVY
.maxLength
< maxLength
) and
550 (not checkTime
or (time
.time() - t
< maxTime
))):
551 # Grow the ivy for this iteration
552 IVY
.grow(ob
, bvhtree
)
554 # Print the proportion of ivy growth to console
555 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
556 print('%0.2f%% of Ivy nodes have grown' %
557 (IVY
.maxLength
/ maxLength
* 100))
559 if IVY
.maxLength
/ maxLength
> 1:
560 print("Halting Growth")
562 # Make an iterator to check if all are alive
563 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
565 # Create the curve and leaf geometry
566 createIvyGeometry(IVY
, growLeaves
)
567 print("Geometry Generation Complete")
569 print("Ivy generated in %0.2f s" % (time
.time() - t
))
571 self
.updateIvy
= False
572 self
.defaultIvy
= False
576 def draw(self
, context
):
579 layout
.prop(self
, "updateIvy", icon
="FILE_REFRESH")
582 class CURVE_PT_IvyGenPanel(Panel
):
583 bl_label
= "Ivy Generator"
584 bl_idname
= "CURVE_PT_IvyGenPanel"
585 bl_space_type
= "VIEW_3D"
586 bl_region_type
= "UI"
587 bl_category
= "Create"
588 bl_context
= "objectmode"
589 bl_options
= {"DEFAULT_CLOSED"}
591 def draw(self
, context
):
593 wm
= context
.window_manager
594 col
= layout
.column(align
=True)
596 prop_new
= col
.operator("curve.ivy_gen", text
="Add New Ivy", icon
="OUTLINER_OB_CURVE")
597 prop_new
.defaultIvy
= False
598 prop_new
.updateIvy
= True
600 prop_def
= col
.operator("curve.ivy_gen", text
="Add New Default Ivy", icon
="CURVE_DATA")
601 prop_def
.defaultIvy
= True
602 prop_def
.updateIvy
= True
604 col
= layout
.column(align
=True)
605 col
.label(text
="Generation Settings:")
606 col
.prop(wm
.ivy_gen_props
, "randomSeed")
607 col
.prop(wm
.ivy_gen_props
, "maxTime")
609 col
= layout
.column(align
=True)
610 col
.label(text
="Size Settings:")
611 col
.prop(wm
.ivy_gen_props
, "maxIvyLength")
612 col
.prop(wm
.ivy_gen_props
, "ivySize")
613 col
.prop(wm
.ivy_gen_props
, "maxFloatLength")
614 col
.prop(wm
.ivy_gen_props
, "maxAdhesionDistance")
616 col
= layout
.column(align
=True)
617 col
.label(text
="Weight Settings:")
618 col
.prop(wm
.ivy_gen_props
, "primaryWeight")
619 col
.prop(wm
.ivy_gen_props
, "randomWeight")
620 col
.prop(wm
.ivy_gen_props
, "gravityWeight")
621 col
.prop(wm
.ivy_gen_props
, "adhesionWeight")
623 col
= layout
.column(align
=True)
624 col
.label(text
="Branch Settings:")
625 col
.prop(wm
.ivy_gen_props
, "branchingProbability")
626 col
.prop(wm
.ivy_gen_props
, "ivyBranchSize")
628 col
= layout
.column(align
=True)
629 col
.prop(wm
.ivy_gen_props
, "growLeaves")
631 if wm
.ivy_gen_props
.growLeaves
:
632 col
= layout
.column(align
=True)
633 col
.label(text
="Leaf Settings:")
634 col
.prop(wm
.ivy_gen_props
, "ivyLeafSize")
635 col
.prop(wm
.ivy_gen_props
, "leafProbability")
638 class IvyGenProperties(PropertyGroup
):
639 maxIvyLength
: FloatProperty(
640 name
="Max Ivy Length",
641 description
="Maximum ivy length in Blender Units",
648 primaryWeight
: FloatProperty(
649 name
="Primary Weight",
650 description
="Weighting given to the current direction",
655 randomWeight
: FloatProperty(
656 name
="Random Weight",
657 description
="Weighting given to the random direction",
662 gravityWeight
: FloatProperty(
663 name
="Gravity Weight",
664 description
="Weighting given to the gravity direction",
669 adhesionWeight
: FloatProperty(
670 name
="Adhesion Weight",
671 description
="Weighting given to the adhesion direction",
676 branchingProbability
: FloatProperty(
677 name
="Branching Probability",
678 description
="Probability of a new branch forming",
683 leafProbability
: FloatProperty(
684 name
="Leaf Probability",
685 description
="Probability of a leaf forming",
690 ivySize
: FloatProperty(
692 description
="The length of an ivy segment in Blender"
699 ivyLeafSize
: FloatProperty(
700 name
="Ivy Leaf Size",
701 description
="The size of the ivy leaves",
707 ivyBranchSize
: FloatProperty(
708 name
="Ivy Branch Size",
709 description
="The size of the ivy branches",
715 maxFloatLength
: FloatProperty(
716 name
="Max Float Length",
717 description
="The maximum distance that a branch "
718 "can live while floating",
723 maxAdhesionDistance
: FloatProperty(
724 name
="Max Adhesion Length",
725 description
="The maximum distance that a branch "
726 "will feel the effects of adhesion",
732 randomSeed
: IntProperty(
734 description
="The seed governing random generation",
738 maxTime
: FloatProperty(
740 description
="The maximum time to run the generation for "
741 "in seconds generation (0.0 = Disabled)",
746 growLeaves
: BoolProperty(
748 description
="Grow leaves or not",
762 bpy
.utils
.register_class(cls
)
764 bpy
.types
.WindowManager
.ivy_gen_props
= PointerProperty(
765 type=IvyGenProperties
770 del bpy
.types
.WindowManager
.ivy_gen_props
772 for cls
in reversed(classes
):
773 bpy
.utils
.unregister_class(cls
)
776 if __name__
== "__main__":