1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
23 "author": "testscreenings, PKHG, TrumanBlending",
25 "blender": (2, 59, 0),
26 "location": "View3D > Add > Curve",
27 "description": "Adds generated ivy to a mesh object starting "
30 "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/"
31 "Scripts/Curve/Ivy_Gen",
32 "category": "Add Curve",
37 from bpy
.props
import (
42 from mathutils
import (
46 from collections
import deque
58 def createIvyGeometry(IVY
, growLeaves
):
59 """Create the curve geometry for IVY"""
60 # Compute the local size and the gauss weight filter
61 # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
62 gaussWeight
= (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
64 # Create a new curve and intialise it
65 curve
= bpy
.data
.curves
.new("IVY", type='CURVE')
66 curve
.dimensions
= '3D'
68 curve
.fill_mode
= 'FULL'
69 curve
.resolution_u
= 4
72 # Create the ivy leaves
73 # Order location of the vertices
74 signList
= ((-1.0, +1.0),
81 # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
83 # Initialise the vertex and face lists
86 # Store the methods for faster calling
87 addV
= vertList
.extend
88 rotMat
= Matrix
.Rotation
90 # Loop over all roots to generate its nodes
91 for root
in IVY
.ivyRoots
:
92 # Only grow if more than one node
93 numNodes
= len(root
.ivyNodes
)
95 # Calculate the local radius
96 local_ivyBranchRadius
= 1.0 / (root
.parents
+ 1) + 1.0
97 prevIvyLength
= 1.0 / root
.ivyNodes
[-1].length
98 splineVerts
= [ax
for n
in root
.ivyNodes
for ax
in n
.pos
.to_4d()]
100 radiusConstant
= local_ivyBranchRadius
* IVY
.ivyBranchSize
101 splineRadii
= [radiusConstant
* (1.3 - n
.length
* prevIvyLength
)
102 for n
in root
.ivyNodes
]
104 # Add the poly curve and set coords and radii
105 newSpline
= curve
.splines
.new(type='POLY')
106 newSpline
.points
.add(len(splineVerts
) // 4 - 1)
107 newSpline
.points
.foreach_set('co', splineVerts
)
108 newSpline
.points
.foreach_set('radius', splineRadii
)
110 # Loop over all nodes in the root
111 for i
, n
in enumerate(root
.ivyNodes
):
112 for k
in range(len(gaussWeight
)):
113 idx
= max(0, min(i
+ k
- 5, numNodes
- 1))
114 n
.smoothAdhesionVector
+= (gaussWeight
[k
] *
115 root
.ivyNodes
[idx
].adhesionVector
)
116 n
.smoothAdhesionVector
/= 56.0
117 n
.adhesionLength
= n
.smoothAdhesionVector
.length
118 n
.smoothAdhesionVector
.normalize()
120 if growLeaves
and (i
< numNodes
- 1):
121 node
= root
.ivyNodes
[i
]
122 nodeNext
= root
.ivyNodes
[i
+ 1]
124 # Find the weight and normalize the smooth adhesion vector
125 weight
= pow(node
.length
* prevIvyLength
, 0.7)
127 # Calculate the ground ivy and the new weight
128 groundIvy
= max(0.0, -node
.smoothAdhesionVector
.z
)
129 weight
+= groundIvy
* pow(1 - node
.length
*
132 # Find the alignment weight
133 alignmentWeight
= node
.adhesionLength
135 # Calculate the needed angles
136 phi
= atan2(node
.smoothAdhesionVector
.y
,
137 node
.smoothAdhesionVector
.x
) - pi
/ 2.0
140 node
.smoothAdhesionVector
.angle(Vector((0, 0, -1)), 0))
142 # Find the size weight
143 sizeWeight
= 1.5 - (cos(2 * pi
* weight
) * 0.5 + 0.5)
145 # Randomise the angles
146 phi
+= (rand_val() - 0.5) * (1.3 - alignmentWeight
)
147 theta
+= (rand_val() - 0.5) * (1.1 - alignmentWeight
)
149 # Calculate the leaf size an append the face to the list
150 leafSize
= IVY
.ivyLeafSize
* sizeWeight
153 # Generate the probability
154 probability
= rand_val()
156 # If we need to grow a leaf, do so
157 if (probability
* weight
) > IVY
.leafProbability
:
159 # Generate the random vector
160 randomVector
= Vector((rand_val() - 0.5,
165 # Find the leaf center
166 center
= (node
.pos
.lerp(nodeNext
.pos
, j
/ 10.0) +
167 IVY
.ivyLeafSize
* randomVector
)
169 # For each of the verts, rotate/scale and append
170 basisVecX
= Vector((1, 0, 0))
171 basisVecY
= Vector((0, 1, 0))
173 horiRot
= rotMat(theta
, 3, 'X')
174 vertRot
= rotMat(phi
, 3, 'Z')
176 basisVecX
.rotate(horiRot
)
177 basisVecY
.rotate(horiRot
)
179 basisVecX
.rotate(vertRot
)
180 basisVecY
.rotate(vertRot
)
182 basisVecX
*= leafSize
183 basisVecY
*= leafSize
185 addV([k1
* basisVecX
+ k2
* basisVecY
+ center
for
188 # Add the object and link to scene
189 newCurve
= bpy
.data
.objects
.new("IVY_Curve", curve
)
190 bpy
.context
.scene
.objects
.link(newCurve
)
193 faceList
= [[4 * i
+ l
for l
in range(4)] for i
in
194 range(len(vertList
) // 4)]
196 # Generate the new leaf mesh and link
197 me
= bpy
.data
.meshes
.new('IvyLeaf')
198 me
.from_pydata(vertList
, [], faceList
)
199 me
.update(calc_edges
=True)
200 ob
= bpy
.data
.objects
.new('IvyLeaf', me
)
201 bpy
.context
.scene
.objects
.link(ob
)
203 me
.uv_textures
.new("Leaves")
205 # Set the uv texture coords
206 # TODO, this is non-functional, default uvs are ok?
209 uv1, uv2, uv3, uv4 = signList
216 def computeBoundingSphere(ob):
219 # Intialise the center
220 center = Vector((0.0, 0.0, 0.0))
221 # Add all vertex coords
222 for v in me.vertices:
224 # Average over all verts
225 center /= len(me.vertices)
226 # Create the iterator and find its max
227 length_iter = ((center - v.co).length for v in me.vertices)
228 radius = max(length_iter)
234 """ The basic class used for each point on the ivy which is grown."""
235 __slots__
= ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
236 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
239 self
.pos
= Vector((0, 0, 0))
240 self
.primaryDir
= Vector((0, 0, 1))
241 self
.adhesionVector
= Vector((0, 0, 0))
242 self
.smoothAdhesionVector
= Vector((0, 0, 0))
244 self
.floatingLength
= 0.0
249 """ The class used to hold all ivy nodes growing from this root point."""
250 __slots__
= ('ivyNodes', 'alive', 'parents')
253 self
.ivyNodes
= deque()
259 """ The class holding all parameters and ivy roots."""
260 __slots__
= ('ivyRoots', 'primaryWeight', 'randomWeight',
261 'gravityWeight', 'adhesionWeight', 'branchingProbability',
262 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
263 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
270 branchingProbability
=0.05,
271 leafProbability
=0.35,
276 maxAdhesionDistance
=1.0):
278 self
.ivyRoots
= deque()
279 self
.primaryWeight
= primaryWeight
280 self
.randomWeight
= randomWeight
281 self
.gravityWeight
= gravityWeight
282 self
.adhesionWeight
= adhesionWeight
283 self
.branchingProbability
= 1 - branchingProbability
284 self
.leafProbability
= 1 - leafProbability
285 self
.ivySize
= ivySize
286 self
.ivyLeafSize
= ivyLeafSize
287 self
.ivyBranchSize
= ivyBranchSize
288 self
.maxFloatLength
= maxFloatLength
289 self
.maxAdhesionDistance
= maxAdhesionDistance
292 # Normalize all the weights only on intialisation
293 sums
= self
.primaryWeight
+ self
.randomWeight
+ self
.adhesionWeight
294 self
.primaryWeight
/= sums
295 self
.randomWeight
/= sums
296 self
.adhesionWeight
/= sums
298 def seed(self
, seedPos
):
299 # Seed the Ivy by making a new root and first node
304 tmpRoot
.ivyNodes
.append(tmpIvy
)
305 self
.ivyRoots
.append(tmpRoot
)
308 # Determine the local sizes
309 # local_ivySize = self.ivySize # * radius
310 # local_maxFloatLength = self.maxFloatLength # * radius
311 # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
313 for root
in self
.ivyRoots
:
314 # Make sure the root is alive, if not, skip
318 # Get the last node in the current root
319 prevIvy
= root
.ivyNodes
[-1]
321 # If the node is floating for too long, kill the root
322 if prevIvy
.floatingLength
> self
.maxFloatLength
:
325 # Set the primary direction from the last node
326 primaryVector
= prevIvy
.primaryDir
328 # Make the random vector and normalize
329 randomVector
= Vector((rand_val() - 0.5, rand_val() - 0.5,
330 rand_val() - 0.5)) + Vector((0, 0, 0.2))
331 randomVector
.normalize()
333 # Calculate the adhesion vector
334 adhesionVector
= adhesion(prevIvy
.pos
, ob
,
335 self
.maxAdhesionDistance
)
337 # Calculate the growing vector
338 growVector
= self
.ivySize
* (primaryVector
* self
.primaryWeight
+
339 randomVector
* self
.randomWeight
+
340 adhesionVector
* self
.adhesionWeight
)
342 # Find the gravity vector
343 gravityVector
= (self
.ivySize
* self
.gravityWeight
*
345 gravityVector
*= pow(prevIvy
.floatingLength
/ self
.maxFloatLength
,
348 # Determine the new position vector
349 newPos
= prevIvy
.pos
+ growVector
+ gravityVector
351 # Check for collisions with the object
352 climbing
= collision(ob
, prevIvy
.pos
, newPos
)
354 # Update the growing vector for any collisions
355 growVector
= newPos
- prevIvy
.pos
- gravityVector
356 growVector
.normalize()
358 # Create a new IvyNode and set its properties
360 tmpNode
.climb
= climbing
362 tmpNode
.primaryDir
= prevIvy
.primaryDir
.lerp(growVector
, 0.5)
363 tmpNode
.primaryDir
.normalize()
364 tmpNode
.adhesionVector
= adhesionVector
365 tmpNode
.length
= prevIvy
.length
+ (newPos
- prevIvy
.pos
).length
367 if tmpNode
.length
> self
.maxLength
:
368 self
.maxLength
= tmpNode
.length
370 # If the node isn't climbing, update it's floating length
371 # Otherwise set it to 0
373 tmpNode
.floatingLength
= prevIvy
.floatingLength
+ (newPos
-
376 tmpNode
.floatingLength
= 0.0
378 root
.ivyNodes
.append(tmpNode
)
380 # Loop through all roots to check if a new root is generated
381 for root
in self
.ivyRoots
:
382 # Check the root is alive and isn't at high level of recursion
383 if (root
.parents
> 3) or (not root
.alive
):
386 # Check to make sure there's more than 1 node
387 if len(root
.ivyNodes
) > 1:
388 # Loop through all nodes in root to check if new root is grown
389 for node
in root
.ivyNodes
:
390 # Set the last node of the root and find the weighting
391 prevIvy
= root
.ivyNodes
[-1]
392 weight
= 1.0 - (cos(2.0 * pi
* node
.length
/
393 prevIvy
.length
) * 0.5 + 0.5)
395 probability
= rand_val()
397 # Check if a new root is grown and if so, set its values
398 if (probability
* weight
> self
.branchingProbability
):
400 tmpNode
.pos
= node
.pos
401 tmpNode
.floatingLength
= node
.floatingLength
404 tmpRoot
.parents
= root
.parents
+ 1
406 tmpRoot
.ivyNodes
.append(tmpNode
)
407 self
.ivyRoots
.append(tmpRoot
)
411 def adhesion(loc
, ob
, max_l
):
412 # Get transfor vector and transformed loc
413 tran_mat
= ob
.matrix_world
.inverted()
414 tran_loc
= tran_mat
* loc
416 # Compute the adhesion vector by finding the nearest point
417 nearest_result
= ob
.closest_point_on_mesh(tran_loc
, max_l
)
418 adhesion_vector
= Vector((0.0, 0.0, 0.0))
419 if nearest_result
[0]:
420 # Compute the distance to the nearest point
421 adhesion_vector
= ob
.matrix_world
* nearest_result
[1] - loc
422 distance
= adhesion_vector
.length
423 # If it's less than the maximum allowed and not 0, continue
425 # Compute the direction vector between the closest point and loc
426 adhesion_vector
.normalize()
427 adhesion_vector
*= 1.0 - distance
/ max_l
428 # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
429 return adhesion_vector
432 def collision(ob
, pos
, new_pos
):
433 # Check for collision with the object
437 tran_mat
= ob
.matrix_world
.inverted()
438 tran_pos
= tran_mat
* pos
439 tran_new_pos
= tran_mat
* new_pos
440 tran_dir
= tran_new_pos
- tran_pos
442 ray_result
= ob
.ray_cast(tran_pos
, tran_dir
, tran_dir
.length
)
443 # If there's a collision we need to check it
445 # Check whether the collision is going into the object
446 if tran_dir
.dot(ray_result
[2]) < 0.0:
447 # Find projection of the piont onto the plane
448 p0
= tran_new_pos
- (tran_new_pos
-
449 ray_result
[1]).project(ray_result
[2])
450 # Reflect in the plane
451 tran_new_pos
+= 2 * (p0
- tran_new_pos
)
453 new_pos
+= ob
.matrix_world
* tran_new_pos
458 def check_mesh_faces(ob
):
460 if len(me
.polygons
) > 0:
466 class IvyGen(bpy
.types
.Operator
):
467 bl_idname
= "curve.ivy_gen"
469 bl_description
= "Generate Ivy on an Mesh Object"
470 bl_options
= {'REGISTER', 'UNDO'}
472 maxIvyLength
= FloatProperty(
473 name
="Max Ivy Length",
474 description
="Maximum ivy length in Blender Units",
481 primaryWeight
= FloatProperty(
482 name
="Primary Weight",
483 description
="Weighting given to the current direction",
488 randomWeight
= FloatProperty(
489 name
="Random Weight",
490 description
="Weighting given to the random direction",
495 gravityWeight
= FloatProperty(
496 name
="Gravity Weight",
497 description
="Weighting given to the gravity direction",
502 adhesionWeight
= FloatProperty(
503 name
="Adhesion Weight",
504 description
="Weighting given to the adhesion direction",
509 branchingProbability
= FloatProperty(
510 name
="Branching Probability",
511 description
="Probability of a new branch forming",
516 leafProbability
= FloatProperty(
517 name
="Leaf Probability",
518 description
="Probability of a leaf forming",
523 ivySize
= FloatProperty(
525 description
="The length of an ivy segment in Blender"
532 ivyLeafSize
= FloatProperty(
533 name
="Ivy Leaf Size",
534 description
="The size of the ivy leaves",
540 ivyBranchSize
= FloatProperty(
541 name
="Ivy Branch Size",
542 description
="The size of the ivy branches",
548 maxFloatLength
= FloatProperty(
549 name
="Max Float Length",
550 description
="The maximum distance that a branch "
551 "can live while floating",
555 maxAdhesionDistance
= FloatProperty(
556 name
="Max Adhesion Length",
557 description
="The maximum distance that a branch "
558 "will feel the effects of adhesion",
564 randomSeed
= IntProperty(
566 description
="The seed governing random generation",
570 maxTime
= FloatProperty(
572 description
="The maximum time to run the generation for "
573 "in seconds generation (0.0 = Disabled)",
578 growLeaves
= BoolProperty(
580 description
="Grow leaves or not",
583 updateIvy
= BoolProperty(
589 def poll(self
, context
):
590 # Check if there's an object and whether it's a mesh
591 ob
= context
.active_object
592 return ((ob
is not None) and
593 (ob
.type == 'MESH') and
594 (context
.mode
== 'OBJECT'))
596 def invoke(self
, context
, event
):
597 self
.updateIvy
= True
598 return self
.execute(context
)
600 def execute(self
, context
):
601 if not self
.updateIvy
:
602 return {'PASS_THROUGH'}
604 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
605 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
607 # Get the selected object
608 ob
= context
.active_object
610 # Check if the mesh has at least one polygon since some functions
611 # are expecting them in the object's data (see T51753)
612 check_face
= check_mesh_faces(ob
)
613 if check_face
is False:
614 self
.report({'WARNING'},
615 "Mesh Object doesn't have at least one Face. "
616 "Operation Cancelled")
619 # Compute bounding sphere radius
620 # radius = computeBoundingSphere(ob) # Not needed anymore
622 # Get the seeding point
623 seedPoint
= context
.scene
.cursor_location
625 # Fix the random seed
626 rand_seed(self
.randomSeed
)
629 IVY
= Ivy(**self
.as_keywords(ignore
=('randomSeed', 'growLeaves',
630 'maxIvyLength', 'maxTime', 'updateIvy')))
632 # Generate first root and node
636 maxLength
= self
.maxIvyLength
# * radius
638 # If we need to check time set the flag
639 if self
.maxTime
!= 0.0:
644 checkAliveIter
= [True, ]
646 # Grow until 200 roots is reached or backup counter exceeds limit
647 while (any(checkAliveIter
) and
648 (IVY
.maxLength
< maxLength
) and
649 (not checkTime
or (time
.time() - t
< self
.maxTime
))):
650 # Grow the ivy for this iteration
653 # Print the proportion of ivy growth to console
654 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
655 print('%0.2f%% of Ivy nodes have grown' %
656 (IVY
.maxLength
/ maxLength
* 100))
658 if IVY
.maxLength
/ maxLength
> 1:
659 print("Halting Growth")
661 # Make an iterator to check if all are alive
662 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
664 # Create the curve and leaf geometry
665 createIvyGeometry(IVY
, self
.growLeaves
)
666 print("Geometry Generation Complete")
668 print("Ivy generated in %0.2f s" % (time
.time() - t
))
670 self
.updateIvy
= False
674 def draw(self
, context
):
677 layout
.prop(self
, 'updateIvy', icon
='CURVE_DATA')
679 properties
= layout
.operator('curve.ivy_gen', text
="Add New Ivy")
680 properties
.randomSeed
= self
.randomSeed
681 properties
.maxTime
= self
.maxTime
682 properties
.maxIvyLength
= self
.maxIvyLength
683 properties
.ivySize
= self
.ivySize
684 properties
.maxFloatLength
= self
.maxFloatLength
685 properties
.maxAdhesionDistance
= self
.maxAdhesionDistance
686 properties
.primaryWeight
= self
.primaryWeight
687 properties
.randomWeight
= self
.randomWeight
688 properties
.gravityWeight
= self
.gravityWeight
689 properties
.adhesionWeight
= self
.adhesionWeight
690 properties
.branchingProbability
= self
.branchingProbability
691 properties
.leafProbability
= self
.leafProbability
692 properties
.ivyBranchSize
= self
.ivyBranchSize
693 properties
.ivyLeafSize
= self
.ivyLeafSize
694 properties
.updateIvy
= True
696 prop_def
= layout
.operator('curve.ivy_gen', text
="Add New Default Ivy")
697 prop_def
.updateIvy
= True
699 layout
.prop(self
, 'growLeaves')
702 box
.label("Generation Settings:")
703 box
.prop(self
, 'randomSeed')
704 box
.prop(self
, 'maxTime')
707 box
.label("Size Settings:")
708 box
.prop(self
, 'maxIvyLength')
709 box
.prop(self
, 'ivySize')
710 box
.prop(self
, 'maxFloatLength')
711 box
.prop(self
, 'maxAdhesionDistance')
714 box
.label("Weight Settings:")
715 box
.prop(self
, 'primaryWeight')
716 box
.prop(self
, 'randomWeight')
717 box
.prop(self
, 'gravityWeight')
718 box
.prop(self
, 'adhesionWeight')
721 box
.label("Branch Settings:")
722 box
.prop(self
, 'branchingProbability')
723 box
.prop(self
, 'ivyBranchSize')
727 box
.label("Leaf Settings:")
728 box
.prop(self
, 'ivyLeafSize')
729 box
.prop(self
, 'leafProbability')
732 def menu_func(self
, context
):
733 self
.layout
.operator(IvyGen
.bl_idname
, text
="Add Ivy to Mesh",
734 icon
='OUTLINER_DATA_CURVE').updateIvy
= True
738 bpy
.utils
.register_module(__name__
)
739 bpy
.types
.INFO_MT_curve_add
.append(menu_func
)
743 bpy
.types
.INFO_MT_curve_add
.remove(menu_func
)
744 bpy
.utils
.unregister_module(__name__
)
747 if __name__
== "__main__":