1 # SPDX-FileCopyrightText: 2011-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "author": "testscreenings, PKHG, TrumanBlending",
10 "location": "View3D > Sidebar > Ivy Generator (Create Tab)",
11 "description": "Adds generated ivy to a mesh object starting "
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/ivy_gen.html",
15 "category": "Add Curve",
20 from bpy
.types
import (
25 from bpy
.props
import (
31 from mathutils
.bvhtree
import BVHTree
32 from mathutils
import (
36 from collections
import deque
48 def createIvyGeometry(IVY
, growLeaves
):
49 """Create the curve geometry for IVY"""
50 # Compute the local size and the gauss weight filter
51 # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
52 gaussWeight
= (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
54 # Create a new curve and initialise it
55 curve
= bpy
.data
.curves
.new("IVY", type='CURVE')
56 curve
.dimensions
= '3D'
58 curve
.fill_mode
= 'FULL'
59 curve
.resolution_u
= 4
62 # Create the ivy leaves
63 # Order location of the vertices
64 signList
= ((-1.0, +1.0),
71 # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
73 # Initialise the vertex and face lists
76 # Store the methods for faster calling
77 addV
= vertList
.extend
78 rotMat
= Matrix
.Rotation
80 # Loop over all roots to generate its nodes
81 for root
in IVY
.ivyRoots
:
82 # Only grow if more than one node
83 numNodes
= len(root
.ivyNodes
)
85 # Calculate the local radius
86 local_ivyBranchRadius
= 1.0 / (root
.parents
+ 1) + 1.0
87 prevIvyLength
= 1.0 / root
.ivyNodes
[-1].length
88 splineVerts
= [ax
for n
in root
.ivyNodes
for ax
in n
.pos
.to_4d()]
90 radiusConstant
= local_ivyBranchRadius
* IVY
.ivyBranchSize
91 splineRadii
= [radiusConstant
* (1.3 - n
.length
* prevIvyLength
)
92 for n
in root
.ivyNodes
]
94 # Add the poly curve and set coords and radii
95 newSpline
= curve
.splines
.new(type='POLY')
96 newSpline
.points
.add(len(splineVerts
) // 4 - 1)
97 newSpline
.points
.foreach_set('co', splineVerts
)
98 newSpline
.points
.foreach_set('radius', splineRadii
)
100 # Loop over all nodes in the root
101 for i
, n
in enumerate(root
.ivyNodes
):
102 for k
in range(len(gaussWeight
)):
103 idx
= max(0, min(i
+ k
- 5, numNodes
- 1))
104 n
.smoothAdhesionVector
+= (gaussWeight
[k
] *
105 root
.ivyNodes
[idx
].adhesionVector
)
106 n
.smoothAdhesionVector
/= 56.0
107 n
.adhesionLength
= n
.smoothAdhesionVector
.length
108 n
.smoothAdhesionVector
.normalize()
110 if growLeaves
and (i
< numNodes
- 1):
111 node
= root
.ivyNodes
[i
]
112 nodeNext
= root
.ivyNodes
[i
+ 1]
114 # Find the weight and normalize the smooth adhesion vector
115 weight
= pow(node
.length
* prevIvyLength
, 0.7)
117 # Calculate the ground ivy and the new weight
118 groundIvy
= max(0.0, -node
.smoothAdhesionVector
.z
)
119 weight
+= groundIvy
* pow(1 - node
.length
*
122 # Find the alignment weight
123 alignmentWeight
= node
.adhesionLength
125 # Calculate the needed angles
126 phi
= atan2(node
.smoothAdhesionVector
.y
,
127 node
.smoothAdhesionVector
.x
) - pi
/ 2.0
130 node
.smoothAdhesionVector
.angle(Vector((0, 0, -1)), 0))
132 # Find the size weight
133 sizeWeight
= 1.5 - (cos(2 * pi
* weight
) * 0.5 + 0.5)
135 # Randomise the angles
136 phi
+= (rand_val() - 0.5) * (1.3 - alignmentWeight
)
137 theta
+= (rand_val() - 0.5) * (1.1 - alignmentWeight
)
139 # Calculate the leaf size an append the face to the list
140 leafSize
= IVY
.ivyLeafSize
* sizeWeight
143 # Generate the probability
144 probability
= rand_val()
146 # If we need to grow a leaf, do so
147 if (probability
* weight
) > IVY
.leafProbability
:
149 # Generate the random vector
150 randomVector
= Vector((rand_val() - 0.5,
155 # Find the leaf center
156 center
= (node
.pos
.lerp(nodeNext
.pos
, j
/ 10.0) +
157 IVY
.ivyLeafSize
* randomVector
)
159 # For each of the verts, rotate/scale and append
160 basisVecX
= Vector((1, 0, 0))
161 basisVecY
= Vector((0, 1, 0))
163 horiRot
= rotMat(theta
, 3, 'X')
164 vertRot
= rotMat(phi
, 3, 'Z')
166 basisVecX
.rotate(horiRot
)
167 basisVecY
.rotate(horiRot
)
169 basisVecX
.rotate(vertRot
)
170 basisVecY
.rotate(vertRot
)
172 basisVecX
*= leafSize
173 basisVecY
*= leafSize
175 addV([k1
* basisVecX
+ k2
* basisVecY
+ center
for
178 # Add the object and link to scene
179 newCurve
= bpy
.data
.objects
.new("IVY_Curve", curve
)
180 bpy
.context
.collection
.objects
.link(newCurve
)
183 faceList
= [[4 * i
+ l
for l
in range(4)] for i
in
184 range(len(vertList
) // 4)]
186 # Generate the new leaf mesh and link
187 me
= bpy
.data
.meshes
.new('IvyLeaf')
188 me
.from_pydata(vertList
, [], faceList
)
189 me
.update(calc_edges
=True)
190 ob
= bpy
.data
.objects
.new('IvyLeaf', me
)
191 bpy
.context
.collection
.objects
.link(ob
)
193 me
.uv_layers
.new(name
="Leaves")
195 # Set the uv texture coords
196 # TODO, this is non-functional, default uvs are ok?
199 uv1, uv2, uv3, uv4 = signList
206 """ The basic class used for each point on the ivy which is grown."""
207 __slots__
= ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
208 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
211 self
.pos
= Vector((0, 0, 0))
212 self
.primaryDir
= Vector((0, 0, 1))
213 self
.adhesionVector
= Vector((0, 0, 0))
214 self
.smoothAdhesionVector
= Vector((0, 0, 0))
216 self
.floatingLength
= 0.0
221 """ The class used to hold all ivy nodes growing from this root point."""
222 __slots__
= ('ivyNodes', 'alive', 'parents')
225 self
.ivyNodes
= deque()
231 """ The class holding all parameters and ivy roots."""
232 __slots__
= ('ivyRoots', 'primaryWeight', 'randomWeight',
233 'gravityWeight', 'adhesionWeight', 'branchingProbability',
234 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
235 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
242 branchingProbability
=0.05,
243 leafProbability
=0.35,
248 maxAdhesionDistance
=1.0):
250 self
.ivyRoots
= deque()
251 self
.primaryWeight
= primaryWeight
252 self
.randomWeight
= randomWeight
253 self
.gravityWeight
= gravityWeight
254 self
.adhesionWeight
= adhesionWeight
255 self
.branchingProbability
= 1 - branchingProbability
256 self
.leafProbability
= 1 - leafProbability
257 self
.ivySize
= ivySize
258 self
.ivyLeafSize
= ivyLeafSize
259 self
.ivyBranchSize
= ivyBranchSize
260 self
.maxFloatLength
= maxFloatLength
261 self
.maxAdhesionDistance
= maxAdhesionDistance
264 # Normalize all the weights only on initialisation
265 sums
= self
.primaryWeight
+ self
.randomWeight
+ self
.adhesionWeight
266 self
.primaryWeight
/= sums
267 self
.randomWeight
/= sums
268 self
.adhesionWeight
/= sums
270 def seed(self
, seedPos
):
271 # Seed the Ivy by making a new root and first node
276 tmpRoot
.ivyNodes
.append(tmpIvy
)
277 self
.ivyRoots
.append(tmpRoot
)
279 def grow(self
, ob
, bvhtree
):
280 # Determine the local sizes
281 # local_ivySize = self.ivySize # * radius
282 # local_maxFloatLength = self.maxFloatLength # * radius
283 # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
285 for root
in self
.ivyRoots
:
286 # Make sure the root is alive, if not, skip
290 # Get the last node in the current root
291 prevIvy
= root
.ivyNodes
[-1]
293 # If the node is floating for too long, kill the root
294 if prevIvy
.floatingLength
> self
.maxFloatLength
:
297 # Set the primary direction from the last node
298 primaryVector
= prevIvy
.primaryDir
300 # Make the random vector and normalize
301 randomVector
= Vector((rand_val() - 0.5, rand_val() - 0.5,
302 rand_val() - 0.5)) + Vector((0, 0, 0.2))
303 randomVector
.normalize()
305 # Calculate the adhesion vector
306 adhesionVector
= adhesion(
307 prevIvy
.pos
, bvhtree
, self
.maxAdhesionDistance
)
309 # Calculate the growing vector
310 growVector
= self
.ivySize
* (primaryVector
* self
.primaryWeight
+
311 randomVector
* self
.randomWeight
+
312 adhesionVector
* self
.adhesionWeight
)
314 # Find the gravity vector
315 gravityVector
= (self
.ivySize
* self
.gravityWeight
*
317 gravityVector
*= pow(prevIvy
.floatingLength
/ self
.maxFloatLength
,
320 # Determine the new position vector
321 newPos
= prevIvy
.pos
+ growVector
+ gravityVector
323 # Check for collisions with the object
324 climbing
, newPos
= collision(bvhtree
, prevIvy
.pos
, newPos
)
326 # Update the growing vector for any collisions
327 growVector
= newPos
- prevIvy
.pos
- gravityVector
328 growVector
.normalize()
330 # Create a new IvyNode and set its properties
332 tmpNode
.climb
= climbing
334 tmpNode
.primaryDir
= prevIvy
.primaryDir
.lerp(growVector
, 0.5)
335 tmpNode
.primaryDir
.normalize()
336 tmpNode
.adhesionVector
= adhesionVector
337 tmpNode
.length
= prevIvy
.length
+ (newPos
- prevIvy
.pos
).length
339 if tmpNode
.length
> self
.maxLength
:
340 self
.maxLength
= tmpNode
.length
342 # If the node isn't climbing, update it's floating length
343 # Otherwise set it to 0
345 tmpNode
.floatingLength
= prevIvy
.floatingLength
+ (newPos
-
348 tmpNode
.floatingLength
= 0.0
350 root
.ivyNodes
.append(tmpNode
)
352 # Loop through all roots to check if a new root is generated
353 for root
in self
.ivyRoots
:
354 # Check the root is alive and isn't at high level of recursion
355 if (root
.parents
> 3) or (not root
.alive
):
358 # Check to make sure there's more than 1 node
359 if len(root
.ivyNodes
) > 1:
360 # Loop through all nodes in root to check if new root is grown
361 for node
in root
.ivyNodes
:
362 # Set the last node of the root and find the weighting
363 prevIvy
= root
.ivyNodes
[-1]
364 weight
= 1.0 - (cos(2.0 * pi
* node
.length
/
365 prevIvy
.length
) * 0.5 + 0.5)
367 probability
= rand_val()
369 # Check if a new root is grown and if so, set its values
370 if (probability
* weight
> self
.branchingProbability
):
372 tmpNode
.pos
= node
.pos
373 tmpNode
.floatingLength
= node
.floatingLength
376 tmpRoot
.parents
= root
.parents
+ 1
378 tmpRoot
.ivyNodes
.append(tmpNode
)
379 self
.ivyRoots
.append(tmpRoot
)
383 def adhesion(loc
, bvhtree
, max_l
):
384 # Compute the adhesion vector by finding the nearest point
385 nearest_location
, *_
= bvhtree
.find_nearest(loc
, max_l
)
386 adhesion_vector
= Vector((0.0, 0.0, 0.0))
387 if nearest_location
is not None:
388 # Compute the distance to the nearest point
389 adhesion_vector
= nearest_location
- loc
390 distance
= adhesion_vector
.length
391 # If it's less than the maximum allowed and not 0, continue
393 # Compute the direction vector between the closest point and loc
394 adhesion_vector
.normalize()
395 adhesion_vector
*= 1.0 - distance
/ max_l
396 # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
397 return adhesion_vector
400 def collision(bvhtree
, pos
, new_pos
):
401 # Check for collision with the object
404 corrected_new_pos
= new_pos
405 direction
= new_pos
- pos
407 hit_location
, hit_normal
, *_
= bvhtree
.ray_cast(pos
, direction
, direction
.length
)
408 # If there's a collision we need to check it
409 if hit_location
is not None:
410 # Check whether the collision is going into the object
411 if direction
.dot(hit_normal
) < 0.0:
412 reflected_direction
= (new_pos
- hit_location
).reflect(hit_normal
)
414 corrected_new_pos
= hit_location
+ reflected_direction
417 return climbing
, corrected_new_pos
420 def bvhtree_from_object(ob
):
424 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
425 ob_eval
= ob
.evaluated_get(depsgraph
)
426 mesh
= ob_eval
.to_mesh()
428 bm
.transform(ob
.matrix_world
)
430 bvhtree
= BVHTree
.FromBMesh(bm
)
431 ob_eval
.to_mesh_clear()
434 def check_mesh_faces(ob
):
436 if len(me
.polygons
) > 0:
442 class IvyGen(Operator
):
443 bl_idname
= "curve.ivy_gen"
445 bl_description
= "Generate Ivy on an Mesh Object"
446 bl_options
= {'REGISTER', 'UNDO'}
448 updateIvy
: BoolProperty(
450 description
="Update the Ivy location based on the cursor and Panel settings",
453 defaultIvy
: BoolProperty(
455 options
={"HIDDEN", "SKIP_SAVE"},
460 def poll(self
, context
):
461 # Check if there's an object and whether it's a mesh
462 ob
= context
.active_object
463 return ((ob
is not None) and
464 (ob
.type == 'MESH') and
465 (context
.mode
== 'OBJECT'))
467 def invoke(self
, context
, event
):
468 self
.updateIvy
= True
469 return self
.execute(context
)
471 def execute(self
, context
):
472 # scene = context.scene
473 ivyProps
= context
.window_manager
.ivy_gen_props
475 if not self
.updateIvy
:
476 return {'PASS_THROUGH'}
478 # assign the variables, check if it is default
479 # Note: update the values if window_manager props defaults are changed
480 randomSeed
= ivyProps
.randomSeed
if not self
.defaultIvy
else 0
481 maxTime
= ivyProps
.maxTime
if not self
.defaultIvy
else 0
482 maxIvyLength
= ivyProps
.maxIvyLength
if not self
.defaultIvy
else 1.0
483 ivySize
= ivyProps
.ivySize
if not self
.defaultIvy
else 0.02
484 maxFloatLength
= ivyProps
.maxFloatLength
if not self
.defaultIvy
else 0.5
485 maxAdhesionDistance
= ivyProps
.maxAdhesionDistance
if not self
.defaultIvy
else 1.0
486 primaryWeight
= ivyProps
.primaryWeight
if not self
.defaultIvy
else 0.5
487 randomWeight
= ivyProps
.randomWeight
if not self
.defaultIvy
else 0.2
488 gravityWeight
= ivyProps
.gravityWeight
if not self
.defaultIvy
else 1.0
489 adhesionWeight
= ivyProps
.adhesionWeight
if not self
.defaultIvy
else 0.1
490 branchingProbability
= ivyProps
.branchingProbability
if not self
.defaultIvy
else 0.05
491 leafProbability
= ivyProps
.leafProbability
if not self
.defaultIvy
else 0.35
492 ivyBranchSize
= ivyProps
.ivyBranchSize
if not self
.defaultIvy
else 0.001
493 ivyLeafSize
= ivyProps
.ivyLeafSize
if not self
.defaultIvy
else 0.02
494 growLeaves
= ivyProps
.growLeaves
if not self
.defaultIvy
else True
496 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
497 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
499 # Get the selected object
500 ob
= context
.active_object
501 bvhtree
= bvhtree_from_object(ob
)
503 # Check if the mesh has at least one polygon since some functions
504 # are expecting them in the object's data (see T51753)
505 check_face
= check_mesh_faces(ob
)
506 if check_face
is False:
507 self
.report({'WARNING'},
508 "Mesh Object doesn't have at least one Face. "
509 "Operation Cancelled")
512 # Compute bounding sphere radius
513 # radius = computeBoundingSphere(ob) # Not needed anymore
515 # Get the seeding point
516 seedPoint
= context
.scene
.cursor
.location
518 # Fix the random seed
519 rand_seed(randomSeed
)
523 primaryWeight
=primaryWeight
,
524 randomWeight
=randomWeight
,
525 gravityWeight
=gravityWeight
,
526 adhesionWeight
=adhesionWeight
,
527 branchingProbability
=branchingProbability
,
528 leafProbability
=leafProbability
,
530 ivyLeafSize
=ivyLeafSize
,
531 ivyBranchSize
=ivyBranchSize
,
532 maxFloatLength
=maxFloatLength
,
533 maxAdhesionDistance
=maxAdhesionDistance
535 # Generate first root and node
539 maxLength
= maxIvyLength
# * radius
541 # If we need to check time set the flag
547 checkAliveIter
= [True, ]
549 # Grow until 200 roots is reached or backup counter exceeds limit
550 while (any(checkAliveIter
) and
551 (IVY
.maxLength
< maxLength
) and
552 (not checkTime
or (time
.time() - t
< maxTime
))):
553 # Grow the ivy for this iteration
554 IVY
.grow(ob
, bvhtree
)
556 # Print the proportion of ivy growth to console
557 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
558 print('%0.2f%% of Ivy nodes have grown' %
559 (IVY
.maxLength
/ maxLength
* 100))
561 if IVY
.maxLength
/ maxLength
> 1:
562 print("Halting Growth")
564 # Make an iterator to check if all are alive
565 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
567 # Create the curve and leaf geometry
568 createIvyGeometry(IVY
, growLeaves
)
569 print("Geometry Generation Complete")
571 print("Ivy generated in %0.2f s" % (time
.time() - t
))
573 self
.updateIvy
= False
574 self
.defaultIvy
= False
578 def draw(self
, context
):
581 layout
.prop(self
, "updateIvy", icon
="FILE_REFRESH")
584 class CURVE_PT_IvyGenPanel(Panel
):
585 bl_label
= "Ivy Generator"
586 bl_idname
= "CURVE_PT_IvyGenPanel"
587 bl_space_type
= "VIEW_3D"
588 bl_region_type
= "UI"
589 bl_category
= "Create"
590 bl_context
= "objectmode"
591 bl_options
= {"DEFAULT_CLOSED"}
593 def draw(self
, context
):
595 wm
= context
.window_manager
596 col
= layout
.column(align
=True)
598 prop_new
= col
.operator("curve.ivy_gen", text
="Add New Ivy", icon
="OUTLINER_OB_CURVE")
599 prop_new
.defaultIvy
= False
600 prop_new
.updateIvy
= True
602 prop_def
= col
.operator("curve.ivy_gen", text
="Add New Default Ivy", icon
="CURVE_DATA")
603 prop_def
.defaultIvy
= True
604 prop_def
.updateIvy
= True
606 col
= layout
.column(align
=True)
607 col
.label(text
="Generation Settings:")
608 col
.prop(wm
.ivy_gen_props
, "randomSeed")
609 col
.prop(wm
.ivy_gen_props
, "maxTime")
611 col
= layout
.column(align
=True)
612 col
.label(text
="Size Settings:")
613 col
.prop(wm
.ivy_gen_props
, "maxIvyLength")
614 col
.prop(wm
.ivy_gen_props
, "ivySize")
615 col
.prop(wm
.ivy_gen_props
, "maxFloatLength")
616 col
.prop(wm
.ivy_gen_props
, "maxAdhesionDistance")
618 col
= layout
.column(align
=True)
619 col
.label(text
="Weight Settings:")
620 col
.prop(wm
.ivy_gen_props
, "primaryWeight")
621 col
.prop(wm
.ivy_gen_props
, "randomWeight")
622 col
.prop(wm
.ivy_gen_props
, "gravityWeight")
623 col
.prop(wm
.ivy_gen_props
, "adhesionWeight")
625 col
= layout
.column(align
=True)
626 col
.label(text
="Branch Settings:")
627 col
.prop(wm
.ivy_gen_props
, "branchingProbability")
628 col
.prop(wm
.ivy_gen_props
, "ivyBranchSize")
630 col
= layout
.column(align
=True)
631 col
.prop(wm
.ivy_gen_props
, "growLeaves")
633 if wm
.ivy_gen_props
.growLeaves
:
634 col
= layout
.column(align
=True)
635 col
.label(text
="Leaf Settings:")
636 col
.prop(wm
.ivy_gen_props
, "ivyLeafSize")
637 col
.prop(wm
.ivy_gen_props
, "leafProbability")
640 class IvyGenProperties(PropertyGroup
):
641 maxIvyLength
: FloatProperty(
642 name
="Max Ivy Length",
643 description
="Maximum ivy length in Blender Units",
650 primaryWeight
: FloatProperty(
651 name
="Primary Weight",
652 description
="Weighting given to the current direction",
657 randomWeight
: FloatProperty(
658 name
="Random Weight",
659 description
="Weighting given to the random direction",
664 gravityWeight
: FloatProperty(
665 name
="Gravity Weight",
666 description
="Weighting given to the gravity direction",
671 adhesionWeight
: FloatProperty(
672 name
="Adhesion Weight",
673 description
="Weighting given to the adhesion direction",
678 branchingProbability
: FloatProperty(
679 name
="Branching Probability",
680 description
="Probability of a new branch forming",
685 leafProbability
: FloatProperty(
686 name
="Leaf Probability",
687 description
="Probability of a leaf forming",
692 ivySize
: FloatProperty(
694 description
="The length of an ivy segment in Blender"
701 ivyLeafSize
: FloatProperty(
702 name
="Ivy Leaf Size",
703 description
="The size of the ivy leaves",
709 ivyBranchSize
: FloatProperty(
710 name
="Ivy Branch Size",
711 description
="The size of the ivy branches",
717 maxFloatLength
: FloatProperty(
718 name
="Max Float Length",
719 description
="The maximum distance that a branch "
720 "can live while floating",
725 maxAdhesionDistance
: FloatProperty(
726 name
="Max Adhesion Length",
727 description
="The maximum distance that a branch "
728 "will feel the effects of adhesion",
734 randomSeed
: IntProperty(
736 description
="The seed governing random generation",
740 maxTime
: FloatProperty(
742 description
="The maximum time to run the generation for "
743 "in seconds generation (0.0 = Disabled)",
748 growLeaves
: BoolProperty(
750 description
="Grow leaves or not",
764 bpy
.utils
.register_class(cls
)
766 bpy
.types
.WindowManager
.ivy_gen_props
= PointerProperty(
767 type=IvyGenProperties
772 del bpy
.types
.WindowManager
.ivy_gen_props
774 for cls
in reversed(classes
):
775 bpy
.utils
.unregister_class(cls
)
778 if __name__
== "__main__":