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 "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
[0]:
406 # Compute the distance to the nearest point
407 adhesion_vector
= ob
.matrix_world
* nearest_result
[1] - 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[3])
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
426 tran_dir
= tran_new_pos
- tran_pos
428 ray_result
= ob
.ray_cast(tran_pos
, tran_dir
, tran_dir
.length
)
429 # If there's a collision we need to check it
431 # Check whether the collision is going into the object
432 if tran_dir
.dot(ray_result
[2]) < 0.0:
433 # Find projection of the piont onto the plane
434 p0
= tran_new_pos
- (tran_new_pos
-
435 ray_result
[1]).project(ray_result
[2])
436 # Reflect in the plane
437 tran_new_pos
+= 2 * (p0
- tran_new_pos
)
439 new_pos
+= ob
.matrix_world
* tran_new_pos
444 class IvyGen(bpy
.types
.Operator
):
445 bl_idname
= "curve.ivy_gen"
447 bl_options
= {'REGISTER', 'UNDO'}
449 maxIvyLength
= FloatProperty(name
="Max Ivy Length",
450 description
="Maximum ivy length in Blender Units",
456 primaryWeight
= FloatProperty(name
="Primary Weight",
457 description
="Weighting given to the current direction",
461 randomWeight
= FloatProperty(name
="Random Weight",
462 description
="Weighting given to the random direction",
466 gravityWeight
= FloatProperty(name
="Gravity Weight",
467 description
="Weighting given to the gravity direction",
471 adhesionWeight
= FloatProperty(name
="Adhesion Weight",
472 description
="Weighting given to the adhesion direction",
476 branchingProbability
= FloatProperty(name
="Branching Probability",
477 description
="Probability of a new branch forming",
481 leafProbability
= FloatProperty(name
="Leaf Probability",
482 description
="Probability of a leaf forming",
486 ivySize
= FloatProperty(name
="Ivy Size",
487 description
=("The length of an ivy segment in Blender"
493 ivyLeafSize
= FloatProperty(name
="Ivy Leaf Size",
494 description
="The size of the ivy leaves",
499 ivyBranchSize
= FloatProperty(name
="Ivy Branch Size",
500 description
="The size of the ivy branches",
505 maxFloatLength
= FloatProperty(name
="Max Float Length",
506 description
=("The maximum distance that a branch "
507 "can live while floating"),
511 maxAdhesionDistance
= FloatProperty(name
="Max Adhesion Length",
512 description
=("The maximum distance that a branch "
513 "will feel the effects of adhesion"),
518 randomSeed
= IntProperty(name
="Random Seed",
519 description
="The seed governing random generation",
522 maxTime
= FloatProperty(name
="Maximum Time",
523 description
=("The maximum time to run the generation for "
524 "in seconds generation (0.0 = Disabled)"),
528 growLeaves
= BoolProperty(name
="Grow Leaves",
529 description
="Grow leaves or not",
531 updateIvy
= BoolProperty(name
="Update Ivy", default
=False)
534 def poll(self
, context
):
535 # Check if there's an object and whether it's a mesh
536 ob
= context
.active_object
537 return ((ob
is not None) and
538 (ob
.type == 'MESH') and
539 (context
.mode
== 'OBJECT'))
541 def execute(self
, context
):
542 if not self
.updateIvy
:
543 return {'PASS_THROUGH'}
545 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
546 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
548 # Get the selected object
549 ob
= context
.active_object
551 # Compute bounding sphere radius
552 #radius = computeBoundingSphere(ob) # Not needed anymore
554 # Get the seeding point
555 seedPoint
= context
.scene
.cursor_location
557 # Fix the random seed
558 rand_seed(self
.randomSeed
)
561 IVY
= Ivy(**self
.as_keywords(ignore
=('randomSeed', 'growLeaves',
562 'maxIvyLength', 'maxTime', 'updateIvy')))
564 # Generate first root and node
568 maxLength
= self
.maxIvyLength
# * radius
570 # If we need to check time set the flag
571 if self
.maxTime
!= 0.0:
576 checkAliveIter
= [True, ]
578 # Grow until 200 roots is reached or backup counter exceeds limit
579 while (any(checkAliveIter
) and
580 (IVY
.maxLength
< maxLength
) and
581 (not checkTime
or (time
.time() - t
< self
.maxTime
))):
582 # Grow the ivy for this iteration
585 # Print the proportion of ivy growth to console
586 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
587 print('%0.2f%% of Ivy nodes have grown' %
588 (IVY
.maxLength
/ maxLength
* 100))
590 if IVY
.maxLength
/ maxLength
> 1:
591 print("Halting Growth")
593 # Make an iterator to check if all are alive
594 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
596 # Create the curve and leaf geometry
597 createIvyGeometry(IVY
, self
.growLeaves
)
598 print("Geometry Generation Complete")
600 print("Ivy generated in %0.2f s" % (time
.time() - t
))
602 self
.updateIvy
= False
606 def draw(self
, context
):
609 layout
.prop(self
, 'updateIvy', icon
='CURVE_DATA')
611 properties
= layout
.operator('curve.ivy_gen', text
="Add New Ivy")
612 properties
.randomSeed
= self
.randomSeed
613 properties
.maxTime
= self
.maxTime
614 properties
.maxIvyLength
= self
.maxIvyLength
615 properties
.ivySize
= self
.ivySize
616 properties
.maxFloatLength
= self
.maxFloatLength
617 properties
.maxAdhesionDistance
= self
.maxAdhesionDistance
618 properties
.primaryWeight
= self
.primaryWeight
619 properties
.randomWeight
= self
.randomWeight
620 properties
.gravityWeight
= self
.gravityWeight
621 properties
.adhesionWeight
= self
.adhesionWeight
622 properties
.branchingProbability
= self
.branchingProbability
623 properties
.leafProbability
= self
.leafProbability
624 properties
.ivyBranchSize
= self
.ivyBranchSize
625 properties
.ivyLeafSize
= self
.ivyLeafSize
626 properties
.updateIvy
= True
628 prop_def
= layout
.operator('curve.ivy_gen', text
="Add New Default Ivy")
629 prop_def
.updateIvy
= True
631 layout
.prop(self
, 'growLeaves')
634 box
.label("Generation Settings:")
635 box
.prop(self
, 'randomSeed')
636 box
.prop(self
, 'maxTime')
639 box
.label("Size Settings:")
640 box
.prop(self
, 'maxIvyLength')
641 box
.prop(self
, 'ivySize')
642 box
.prop(self
, 'maxFloatLength')
643 box
.prop(self
, 'maxAdhesionDistance')
646 box
.label("Weight Settings:")
647 box
.prop(self
, 'primaryWeight')
648 box
.prop(self
, 'randomWeight')
649 box
.prop(self
, 'gravityWeight')
650 box
.prop(self
, 'adhesionWeight')
653 box
.label("Branch Settings:")
654 box
.prop(self
, 'branchingProbability')
655 box
.prop(self
, 'ivyBranchSize')
659 box
.label("Leaf Settings:")
660 box
.prop(self
, 'ivyLeafSize')
661 box
.prop(self
, 'leafProbability')
664 def menu_func(self
, context
):
665 self
.layout
.operator(IvyGen
.bl_idname
, text
="Add Ivy to Mesh",
666 icon
='PLUGIN').updateIvy
= True
670 bpy
.utils
.register_module(__name__
)
671 bpy
.types
.INFO_MT_curve_add
.append(menu_func
)
675 bpy
.types
.INFO_MT_curve_add
.remove(menu_func
)
676 bpy
.utils
.unregister_module(__name__
)
679 if __name__
== "__main__":