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": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
31 "Scripts/Curve/Ivy_Gen",
32 "tracker_url": "https://developer.blender.org/T27234",
33 "category": "Add Curve"}
37 from bpy
.props
import FloatProperty
, IntProperty
, BoolProperty
38 from mathutils
import Vector
, Matrix
39 from collections
import deque
40 from math
import pow, cos
, pi
, atan2
41 from random
import random
as rand_val
, seed
as rand_seed
45 def createIvyGeometry(IVY
, growLeaves
):
46 """Create the curve geometry for IVY"""
47 # Compute the local size and the gauss weight filter
48 #local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
49 gaussWeight
= (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
51 # Create a new curve and intialise it
52 curve
= bpy
.data
.curves
.new("IVY", type='CURVE')
53 curve
.dimensions
= '3D'
55 curve
.fill_mode
= 'FULL'
56 curve
.resolution_u
= 4
59 # Create the ivy leaves
60 # Order location of the vertices
61 signList
= ((-1.0, +1.0),
68 #local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
70 # Initialise the vertex and face lists
73 # Store the methods for faster calling
74 addV
= vertList
.extend
75 rotMat
= Matrix
.Rotation
77 # Loop over all roots to generate its nodes
78 for root
in IVY
.ivyRoots
:
79 # Only grow if more than one node
80 numNodes
= len(root
.ivyNodes
)
82 # Calculate the local radius
83 local_ivyBranchRadius
= 1.0 / (root
.parents
+ 1) + 1.0
84 prevIvyLength
= 1.0 / root
.ivyNodes
[-1].length
85 splineVerts
= [ax
for n
in root
.ivyNodes
for ax
in n
.pos
.to_4d()]
87 radiusConstant
= local_ivyBranchRadius
* IVY
.ivyBranchSize
88 splineRadii
= [radiusConstant
* (1.3 - n
.length
* prevIvyLength
)
89 for n
in root
.ivyNodes
]
91 # Add the poly curve and set coords and radii
92 newSpline
= curve
.splines
.new(type='POLY')
93 newSpline
.points
.add(len(splineVerts
) // 4 - 1)
94 newSpline
.points
.foreach_set('co', splineVerts
)
95 newSpline
.points
.foreach_set('radius', splineRadii
)
97 # Loop over all nodes in the root
98 for i
, n
in enumerate(root
.ivyNodes
):
99 for k
in range(len(gaussWeight
)):
100 idx
= max(0, min(i
+ k
- 5, numNodes
- 1))
101 n
.smoothAdhesionVector
+= (gaussWeight
[k
] *
102 root
.ivyNodes
[idx
].adhesionVector
)
103 n
.smoothAdhesionVector
/= 56.0
104 n
.adhesionLength
= n
.smoothAdhesionVector
.length
105 n
.smoothAdhesionVector
.normalize()
107 if growLeaves
and (i
< numNodes
- 1):
108 node
= root
.ivyNodes
[i
]
109 nodeNext
= root
.ivyNodes
[i
+ 1]
111 # Find the weight and normalize the smooth adhesion vector
112 weight
= pow(node
.length
* prevIvyLength
, 0.7)
114 # Calculate the ground ivy and the new weight
115 groundIvy
= max(0.0, -node
.smoothAdhesionVector
.z
)
116 weight
+= groundIvy
* pow(1 - node
.length
*
119 # Find the alignment weight
120 alignmentWeight
= node
.adhesionLength
122 # Calculate the needed angles
123 phi
= atan2(node
.smoothAdhesionVector
.y
,
124 node
.smoothAdhesionVector
.x
) - pi
/ 2.0
127 node
.smoothAdhesionVector
.angle(Vector((0, 0, -1)), 0))
129 # Find the size weight
130 sizeWeight
= 1.5 - (cos(2 * pi
* weight
) * 0.5 + 0.5)
132 # Randomise the angles
133 phi
+= (rand_val() - 0.5) * (1.3 - alignmentWeight
)
134 theta
+= (rand_val() - 0.5) * (1.1 - alignmentWeight
)
136 # Calculate the leaf size an append the face to the list
137 leafSize
= IVY
.ivyLeafSize
* sizeWeight
140 # Generate the probability
141 probability
= rand_val()
143 # If we need to grow a leaf, do so
144 if (probability
* weight
) > IVY
.leafProbability
:
146 # Generate the random vector
147 randomVector
= Vector((rand_val() - 0.5,
152 # Find the leaf center
153 center
= (node
.pos
.lerp(nodeNext
.pos
, j
/ 10.0) +
154 IVY
.ivyLeafSize
* randomVector
)
156 # For each of the verts, rotate/scale and append
157 basisVecX
= Vector((1, 0, 0))
158 basisVecY
= Vector((0, 1, 0))
160 horiRot
= rotMat(theta
, 3, 'X')
161 vertRot
= rotMat(phi
, 3, 'Z')
163 basisVecX
.rotate(horiRot
)
164 basisVecY
.rotate(horiRot
)
166 basisVecX
.rotate(vertRot
)
167 basisVecY
.rotate(vertRot
)
169 basisVecX
*= leafSize
170 basisVecY
*= leafSize
172 addV([k1
* basisVecX
+ k2
* basisVecY
+ center
for
175 # Add the object and link to scene
176 newCurve
= bpy
.data
.objects
.new("IVY_Curve", curve
)
177 bpy
.context
.scene
.objects
.link(newCurve
)
180 faceList
= [[4 * i
+ l
for l
in range(4)] for i
in
181 range(len(vertList
) // 4)]
183 # Generate the new leaf mesh and link
184 me
= bpy
.data
.meshes
.new('IvyLeaf')
185 me
.from_pydata(vertList
, [], faceList
)
186 me
.update(calc_edges
=True)
187 ob
= bpy
.data
.objects
.new('IvyLeaf', me
)
188 bpy
.context
.scene
.objects
.link(ob
)
190 me
.uv_textures
.new("Leaves")
192 # Set the uv texture coords
193 # TODO, this is non-functional, default uvs are ok?
196 uv1, uv2, uv3, uv4 = signList
202 def computeBoundingSphere(ob):
205 # Intialise the center
206 center = Vector((0.0, 0.0, 0.0))
207 # Add all vertex coords
208 for v in me.vertices:
210 # Average over all verts
211 center /= len(me.vertices)
212 # Create the iterator and find its max
213 length_iter = ((center - v.co).length for v in me.vertices)
214 radius = max(length_iter)
220 """ The basic class used for each point on the ivy which is grown."""
221 __slots__
= ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
222 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
225 self
.pos
= Vector((0, 0, 0))
226 self
.primaryDir
= Vector((0, 0, 1))
227 self
.adhesionVector
= Vector((0, 0, 0))
228 self
.smoothAdhesionVector
= Vector((0, 0, 0))
230 self
.floatingLength
= 0.0
235 """ The class used to hold all ivy nodes growing from this root point."""
236 __slots__
= ('ivyNodes', 'alive', 'parents')
239 self
.ivyNodes
= deque()
245 """ The class holding all parameters and ivy roots."""
246 __slots__
= ('ivyRoots', 'primaryWeight', 'randomWeight',
247 'gravityWeight', 'adhesionWeight', 'branchingProbability',
248 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
249 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
256 branchingProbability
=0.05,
257 leafProbability
=0.35,
262 maxAdhesionDistance
=1.0):
264 self
.ivyRoots
= deque()
265 self
.primaryWeight
= primaryWeight
266 self
.randomWeight
= randomWeight
267 self
.gravityWeight
= gravityWeight
268 self
.adhesionWeight
= adhesionWeight
269 self
.branchingProbability
= 1 - branchingProbability
270 self
.leafProbability
= 1 - leafProbability
271 self
.ivySize
= ivySize
272 self
.ivyLeafSize
= ivyLeafSize
273 self
.ivyBranchSize
= ivyBranchSize
274 self
.maxFloatLength
= maxFloatLength
275 self
.maxAdhesionDistance
= maxAdhesionDistance
278 # Normalize all the weights only on intialisation
279 sum = self
.primaryWeight
+ self
.randomWeight
+ self
.adhesionWeight
280 self
.primaryWeight
/= sum
281 self
.randomWeight
/= sum
282 self
.adhesionWeight
/= sum
284 def seed(self
, seedPos
):
285 # Seed the Ivy by making a new root and first node
290 tmpRoot
.ivyNodes
.append(tmpIvy
)
291 self
.ivyRoots
.append(tmpRoot
)
294 # Determine the local sizes
295 #local_ivySize = self.ivySize # * radius
296 #local_maxFloatLength = self.maxFloatLength # * radius
297 #local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
299 for root
in self
.ivyRoots
:
300 # Make sure the root is alive, if not, skip
304 # Get the last node in the current root
305 prevIvy
= root
.ivyNodes
[-1]
307 # If the node is floating for too long, kill the root
308 if prevIvy
.floatingLength
> self
.maxFloatLength
:
311 # Set the primary direction from the last node
312 primaryVector
= prevIvy
.primaryDir
314 # Make the random vector and normalize
315 randomVector
= Vector((rand_val() - 0.5, rand_val() - 0.5,
316 rand_val() - 0.5)) + Vector((0, 0, 0.2))
317 randomVector
.normalize()
319 # Calculate the adhesion vector
320 adhesionVector
= adhesion(prevIvy
.pos
, ob
,
321 self
.maxAdhesionDistance
)
323 # Calculate the growing vector
324 growVector
= self
.ivySize
* (primaryVector
* self
.primaryWeight
+
325 randomVector
* self
.randomWeight
+
326 adhesionVector
* self
.adhesionWeight
)
328 # Find the gravity vector
329 gravityVector
= (self
.ivySize
* self
.gravityWeight
*
331 gravityVector
*= pow(prevIvy
.floatingLength
/ self
.maxFloatLength
,
334 # Determine the new position vector
335 newPos
= prevIvy
.pos
+ growVector
+ gravityVector
337 # Check for collisions with the object
338 climbing
= collision(ob
, prevIvy
.pos
, newPos
)
340 # Update the growing vector for any collisions
341 growVector
= newPos
- prevIvy
.pos
- gravityVector
342 growVector
.normalize()
344 # Create a new IvyNode and set its properties
346 tmpNode
.climb
= climbing
348 tmpNode
.primaryDir
= prevIvy
.primaryDir
.lerp(growVector
, 0.5)
349 tmpNode
.primaryDir
.normalize()
350 tmpNode
.adhesionVector
= adhesionVector
351 tmpNode
.length
= prevIvy
.length
+ (newPos
- prevIvy
.pos
).length
353 if tmpNode
.length
> self
.maxLength
:
354 self
.maxLength
= tmpNode
.length
356 # If the node isn't climbing, update it's floating length
357 # Otherwise set it to 0
359 tmpNode
.floatingLength
= prevIvy
.floatingLength
+ (newPos
-
362 tmpNode
.floatingLength
= 0.0
364 root
.ivyNodes
.append(tmpNode
)
366 # Loop through all roots to check if a new root is generated
367 for root
in self
.ivyRoots
:
368 # Check the root is alive and isn't at high level of recursion
369 if (root
.parents
> 3) or (not root
.alive
):
372 # Check to make sure there's more than 1 node
373 if len(root
.ivyNodes
) > 1:
374 # Loop through all nodes in root to check if new root is grown
375 for node
in root
.ivyNodes
:
376 # Set the last node of the root and find the weighting
377 prevIvy
= root
.ivyNodes
[-1]
378 weight
= 1.0 - (cos(2.0 * pi
* node
.length
/
379 prevIvy
.length
) * 0.5 + 0.5)
381 probability
= rand_val()
383 # Check if a new root is grown and if so, set its values
384 if (probability
* weight
> self
.branchingProbability
):
386 tmpNode
.pos
= node
.pos
387 tmpNode
.floatingLength
= node
.floatingLength
390 tmpRoot
.parents
= root
.parents
+ 1
392 tmpRoot
.ivyNodes
.append(tmpNode
)
393 self
.ivyRoots
.append(tmpRoot
)
397 def adhesion(loc
, ob
, max_l
):
398 # Get transfor vector and transformed loc
399 tran_mat
= ob
.matrix_world
.inverted()
400 tran_loc
= tran_mat
* loc
402 # Compute the adhesion vector by finding the nearest point
403 nearest_result
= ob
.closest_point_on_mesh(tran_loc
, max_l
)
404 adhesion_vector
= Vector((0.0, 0.0, 0.0))
405 if nearest_result
[2] != -1:
406 # Compute the distance to the nearest point
407 adhesion_vector
= ob
.matrix_world
* nearest_result
[0] - loc
408 distance
= adhesion_vector
.length
409 # If it's less than the maximum allowed and not 0, continue
411 # Compute the direction vector between the closest point and loc
412 adhesion_vector
.normalize()
413 adhesion_vector
*= 1.0 - distance
/ max_l
414 #adhesion_vector *= getFaceWeight(ob.data, nearest_result[2])
415 return adhesion_vector
418 def collision(ob
, pos
, new_pos
):
419 # Check for collision with the object
423 tran_mat
= ob
.matrix_world
.inverted()
424 tran_pos
= tran_mat
* pos
425 tran_new_pos
= tran_mat
* new_pos
427 ray_result
= ob
.ray_cast(tran_pos
, tran_new_pos
)
428 # If there's a collision we need to check it
429 if ray_result
[2] != -1:
430 # Check whether the collision is going into the object
431 if (tran_new_pos
- tran_pos
).dot(ray_result
[1]) < 0.0:
432 # Find projection of the piont onto the plane
433 p0
= tran_new_pos
- (tran_new_pos
-
434 ray_result
[0]).project(ray_result
[1])
435 # Reflect in the plane
436 tran_new_pos
+= 2 * (p0
- tran_new_pos
)
438 new_pos
+= ob
.matrix_world
* tran_new_pos
443 class IvyGen(bpy
.types
.Operator
):
444 bl_idname
= "curve.ivy_gen"
446 bl_options
= {'REGISTER', 'UNDO'}
448 maxIvyLength
= FloatProperty(name
="Max Ivy Length",
449 description
="Maximum ivy length in Blender Units",
455 primaryWeight
= FloatProperty(name
="Primary Weight",
456 description
="Weighting given to the current direction",
460 randomWeight
= FloatProperty(name
="Random Weight",
461 description
="Weighting given to the random direction",
465 gravityWeight
= FloatProperty(name
="Gravity Weight",
466 description
="Weighting given to the gravity direction",
470 adhesionWeight
= FloatProperty(name
="Adhesion Weight",
471 description
="Weighting given to the adhesion direction",
475 branchingProbability
= FloatProperty(name
="Branching Probability",
476 description
="Probability of a new branch forming",
480 leafProbability
= FloatProperty(name
="Leaf Probability",
481 description
="Probability of a leaf forming",
485 ivySize
= FloatProperty(name
="Ivy Size",
486 description
=("The length of an ivy segment in Blender"
492 ivyLeafSize
= FloatProperty(name
="Ivy Leaf Size",
493 description
="The size of the ivy leaves",
498 ivyBranchSize
= FloatProperty(name
="Ivy Branch Size",
499 description
="The size of the ivy branches",
504 maxFloatLength
= FloatProperty(name
="Max Float Length",
505 description
=("The maximum distance that a branch "
506 "can live while floating"),
510 maxAdhesionDistance
= FloatProperty(name
="Max Adhesion Length",
511 description
=("The maximum distance that a branch "
512 "will feel the effects of adhesion"),
517 randomSeed
= IntProperty(name
="Random Seed",
518 description
="The seed governing random generation",
521 maxTime
= FloatProperty(name
="Maximum Time",
522 description
=("The maximum time to run the generation for "
523 "in seconds generation (0.0 = Disabled)"),
527 growLeaves
= BoolProperty(name
="Grow Leaves",
528 description
="Grow leaves or not",
530 updateIvy
= BoolProperty(name
="Update Ivy", default
=False)
533 def poll(self
, context
):
534 # Check if there's an object and whether it's a mesh
535 ob
= context
.active_object
536 return ((ob
is not None) and
537 (ob
.type == 'MESH') and
538 (context
.mode
== 'OBJECT'))
540 def execute(self
, context
):
541 if not self
.updateIvy
:
542 return {'PASS_THROUGH'}
544 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
545 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
547 # Get the selected object
548 ob
= context
.active_object
550 # Compute bounding sphere radius
551 #radius = computeBoundingSphere(ob) # Not needed anymore
553 # Get the seeding point
554 seedPoint
= context
.scene
.cursor_location
556 # Fix the random seed
557 rand_seed(self
.randomSeed
)
560 IVY
= Ivy(**self
.as_keywords(ignore
=('randomSeed', 'growLeaves',
561 'maxIvyLength', 'maxTime', 'updateIvy')))
563 # Generate first root and node
567 maxLength
= self
.maxIvyLength
# * radius
569 # If we need to check time set the flag
570 if self
.maxTime
!= 0.0:
575 checkAliveIter
= [True, ]
577 # Grow until 200 roots is reached or backup counter exceeds limit
578 while (any(checkAliveIter
) and
579 (IVY
.maxLength
< maxLength
) and
580 (not checkTime
or (time
.time() - t
< self
.maxTime
))):
581 # Grow the ivy for this iteration
584 # Print the proportion of ivy growth to console
585 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
586 print('%0.2f%% of Ivy nodes have grown' %
587 (IVY
.maxLength
/ maxLength
* 100))
589 if IVY
.maxLength
/ maxLength
> 1:
590 print("Halting Growth")
592 # Make an iterator to check if all are alive
593 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
595 # Create the curve and leaf geometry
596 createIvyGeometry(IVY
, self
.growLeaves
)
597 print("Geometry Generation Complete")
599 print("Ivy generated in %0.2f s" % (time
.time() - t
))
601 self
.updateIvy
= False
605 def draw(self
, context
):
608 layout
.prop(self
, 'updateIvy', icon
='CURVE_DATA')
610 properties
= layout
.operator('curve.ivy_gen', text
="Add New Ivy")
611 properties
.randomSeed
= self
.randomSeed
612 properties
.maxTime
= self
.maxTime
613 properties
.maxIvyLength
= self
.maxIvyLength
614 properties
.ivySize
= self
.ivySize
615 properties
.maxFloatLength
= self
.maxFloatLength
616 properties
.maxAdhesionDistance
= self
.maxAdhesionDistance
617 properties
.primaryWeight
= self
.primaryWeight
618 properties
.randomWeight
= self
.randomWeight
619 properties
.gravityWeight
= self
.gravityWeight
620 properties
.adhesionWeight
= self
.adhesionWeight
621 properties
.branchingProbability
= self
.branchingProbability
622 properties
.leafProbability
= self
.leafProbability
623 properties
.ivyBranchSize
= self
.ivyBranchSize
624 properties
.ivyLeafSize
= self
.ivyLeafSize
625 properties
.updateIvy
= True
627 prop_def
= layout
.operator('curve.ivy_gen', text
="Add New Default Ivy")
628 prop_def
.updateIvy
= True
630 layout
.prop(self
, 'growLeaves')
633 box
.label("Generation Settings:")
634 box
.prop(self
, 'randomSeed')
635 box
.prop(self
, 'maxTime')
638 box
.label("Size Settings:")
639 box
.prop(self
, 'maxIvyLength')
640 box
.prop(self
, 'ivySize')
641 box
.prop(self
, 'maxFloatLength')
642 box
.prop(self
, 'maxAdhesionDistance')
645 box
.label("Weight Settings:")
646 box
.prop(self
, 'primaryWeight')
647 box
.prop(self
, 'randomWeight')
648 box
.prop(self
, 'gravityWeight')
649 box
.prop(self
, 'adhesionWeight')
652 box
.label("Branch Settings:")
653 box
.prop(self
, 'branchingProbability')
654 box
.prop(self
, 'ivyBranchSize')
658 box
.label("Leaf Settings:")
659 box
.prop(self
, 'ivyLeafSize')
660 box
.prop(self
, 'leafProbability')
663 def menu_func(self
, context
):
664 self
.layout
.operator(IvyGen
.bl_idname
, text
="Add Ivy to Mesh",
665 icon
='PLUGIN').updateIvy
= True
669 bpy
.utils
.register_module(__name__
)
670 bpy
.types
.INFO_MT_curve_add
.append(menu_func
)
674 bpy
.types
.INFO_MT_curve_add
.remove(menu_func
)
675 bpy
.utils
.unregister_module(__name__
)
678 if __name__
== "__main__":