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 > Tool Shelf > Create > Ivy Generator",
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
.types
import (
42 from bpy
.props
import (
48 from mathutils
import (
52 from collections
import deque
64 def createIvyGeometry(IVY
, growLeaves
):
65 """Create the curve geometry for IVY"""
66 # Compute the local size and the gauss weight filter
67 # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
68 gaussWeight
= (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
70 # Create a new curve and intialise it
71 curve
= bpy
.data
.curves
.new("IVY", type='CURVE')
72 curve
.dimensions
= '3D'
74 curve
.fill_mode
= 'FULL'
75 curve
.resolution_u
= 4
78 # Create the ivy leaves
79 # Order location of the vertices
80 signList
= ((-1.0, +1.0),
87 # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
89 # Initialise the vertex and face lists
92 # Store the methods for faster calling
93 addV
= vertList
.extend
94 rotMat
= Matrix
.Rotation
96 # Loop over all roots to generate its nodes
97 for root
in IVY
.ivyRoots
:
98 # Only grow if more than one node
99 numNodes
= len(root
.ivyNodes
)
101 # Calculate the local radius
102 local_ivyBranchRadius
= 1.0 / (root
.parents
+ 1) + 1.0
103 prevIvyLength
= 1.0 / root
.ivyNodes
[-1].length
104 splineVerts
= [ax
for n
in root
.ivyNodes
for ax
in n
.pos
.to_4d()]
106 radiusConstant
= local_ivyBranchRadius
* IVY
.ivyBranchSize
107 splineRadii
= [radiusConstant
* (1.3 - n
.length
* prevIvyLength
)
108 for n
in root
.ivyNodes
]
110 # Add the poly curve and set coords and radii
111 newSpline
= curve
.splines
.new(type='POLY')
112 newSpline
.points
.add(len(splineVerts
) // 4 - 1)
113 newSpline
.points
.foreach_set('co', splineVerts
)
114 newSpline
.points
.foreach_set('radius', splineRadii
)
116 # Loop over all nodes in the root
117 for i
, n
in enumerate(root
.ivyNodes
):
118 for k
in range(len(gaussWeight
)):
119 idx
= max(0, min(i
+ k
- 5, numNodes
- 1))
120 n
.smoothAdhesionVector
+= (gaussWeight
[k
] *
121 root
.ivyNodes
[idx
].adhesionVector
)
122 n
.smoothAdhesionVector
/= 56.0
123 n
.adhesionLength
= n
.smoothAdhesionVector
.length
124 n
.smoothAdhesionVector
.normalize()
126 if growLeaves
and (i
< numNodes
- 1):
127 node
= root
.ivyNodes
[i
]
128 nodeNext
= root
.ivyNodes
[i
+ 1]
130 # Find the weight and normalize the smooth adhesion vector
131 weight
= pow(node
.length
* prevIvyLength
, 0.7)
133 # Calculate the ground ivy and the new weight
134 groundIvy
= max(0.0, -node
.smoothAdhesionVector
.z
)
135 weight
+= groundIvy
* pow(1 - node
.length
*
138 # Find the alignment weight
139 alignmentWeight
= node
.adhesionLength
141 # Calculate the needed angles
142 phi
= atan2(node
.smoothAdhesionVector
.y
,
143 node
.smoothAdhesionVector
.x
) - pi
/ 2.0
146 node
.smoothAdhesionVector
.angle(Vector((0, 0, -1)), 0))
148 # Find the size weight
149 sizeWeight
= 1.5 - (cos(2 * pi
* weight
) * 0.5 + 0.5)
151 # Randomise the angles
152 phi
+= (rand_val() - 0.5) * (1.3 - alignmentWeight
)
153 theta
+= (rand_val() - 0.5) * (1.1 - alignmentWeight
)
155 # Calculate the leaf size an append the face to the list
156 leafSize
= IVY
.ivyLeafSize
* sizeWeight
159 # Generate the probability
160 probability
= rand_val()
162 # If we need to grow a leaf, do so
163 if (probability
* weight
) > IVY
.leafProbability
:
165 # Generate the random vector
166 randomVector
= Vector((rand_val() - 0.5,
171 # Find the leaf center
172 center
= (node
.pos
.lerp(nodeNext
.pos
, j
/ 10.0) +
173 IVY
.ivyLeafSize
* randomVector
)
175 # For each of the verts, rotate/scale and append
176 basisVecX
= Vector((1, 0, 0))
177 basisVecY
= Vector((0, 1, 0))
179 horiRot
= rotMat(theta
, 3, 'X')
180 vertRot
= rotMat(phi
, 3, 'Z')
182 basisVecX
.rotate(horiRot
)
183 basisVecY
.rotate(horiRot
)
185 basisVecX
.rotate(vertRot
)
186 basisVecY
.rotate(vertRot
)
188 basisVecX
*= leafSize
189 basisVecY
*= leafSize
191 addV([k1
* basisVecX
+ k2
* basisVecY
+ center
for
194 # Add the object and link to scene
195 newCurve
= bpy
.data
.objects
.new("IVY_Curve", curve
)
196 bpy
.context
.scene
.objects
.link(newCurve
)
199 faceList
= [[4 * i
+ l
for l
in range(4)] for i
in
200 range(len(vertList
) // 4)]
202 # Generate the new leaf mesh and link
203 me
= bpy
.data
.meshes
.new('IvyLeaf')
204 me
.from_pydata(vertList
, [], faceList
)
205 me
.update(calc_edges
=True)
206 ob
= bpy
.data
.objects
.new('IvyLeaf', me
)
207 bpy
.context
.scene
.objects
.link(ob
)
209 me
.uv_textures
.new("Leaves")
211 # Set the uv texture coords
212 # TODO, this is non-functional, default uvs are ok?
215 uv1, uv2, uv3, uv4 = signList
222 """ The basic class used for each point on the ivy which is grown."""
223 __slots__
= ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
224 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
227 self
.pos
= Vector((0, 0, 0))
228 self
.primaryDir
= Vector((0, 0, 1))
229 self
.adhesionVector
= Vector((0, 0, 0))
230 self
.smoothAdhesionVector
= Vector((0, 0, 0))
232 self
.floatingLength
= 0.0
237 """ The class used to hold all ivy nodes growing from this root point."""
238 __slots__
= ('ivyNodes', 'alive', 'parents')
241 self
.ivyNodes
= deque()
247 """ The class holding all parameters and ivy roots."""
248 __slots__
= ('ivyRoots', 'primaryWeight', 'randomWeight',
249 'gravityWeight', 'adhesionWeight', 'branchingProbability',
250 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
251 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
258 branchingProbability
=0.05,
259 leafProbability
=0.35,
264 maxAdhesionDistance
=1.0):
266 self
.ivyRoots
= deque()
267 self
.primaryWeight
= primaryWeight
268 self
.randomWeight
= randomWeight
269 self
.gravityWeight
= gravityWeight
270 self
.adhesionWeight
= adhesionWeight
271 self
.branchingProbability
= 1 - branchingProbability
272 self
.leafProbability
= 1 - leafProbability
273 self
.ivySize
= ivySize
274 self
.ivyLeafSize
= ivyLeafSize
275 self
.ivyBranchSize
= ivyBranchSize
276 self
.maxFloatLength
= maxFloatLength
277 self
.maxAdhesionDistance
= maxAdhesionDistance
280 # Normalize all the weights only on intialisation
281 sums
= self
.primaryWeight
+ self
.randomWeight
+ self
.adhesionWeight
282 self
.primaryWeight
/= sums
283 self
.randomWeight
/= sums
284 self
.adhesionWeight
/= sums
286 def seed(self
, seedPos
):
287 # Seed the Ivy by making a new root and first node
292 tmpRoot
.ivyNodes
.append(tmpIvy
)
293 self
.ivyRoots
.append(tmpRoot
)
296 # Determine the local sizes
297 # local_ivySize = self.ivySize # * radius
298 # local_maxFloatLength = self.maxFloatLength # * radius
299 # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
301 for root
in self
.ivyRoots
:
302 # Make sure the root is alive, if not, skip
306 # Get the last node in the current root
307 prevIvy
= root
.ivyNodes
[-1]
309 # If the node is floating for too long, kill the root
310 if prevIvy
.floatingLength
> self
.maxFloatLength
:
313 # Set the primary direction from the last node
314 primaryVector
= prevIvy
.primaryDir
316 # Make the random vector and normalize
317 randomVector
= Vector((rand_val() - 0.5, rand_val() - 0.5,
318 rand_val() - 0.5)) + Vector((0, 0, 0.2))
319 randomVector
.normalize()
321 # Calculate the adhesion vector
322 adhesionVector
= adhesion(prevIvy
.pos
, ob
,
323 self
.maxAdhesionDistance
)
325 # Calculate the growing vector
326 growVector
= self
.ivySize
* (primaryVector
* self
.primaryWeight
+
327 randomVector
* self
.randomWeight
+
328 adhesionVector
* self
.adhesionWeight
)
330 # Find the gravity vector
331 gravityVector
= (self
.ivySize
* self
.gravityWeight
*
333 gravityVector
*= pow(prevIvy
.floatingLength
/ self
.maxFloatLength
,
336 # Determine the new position vector
337 newPos
= prevIvy
.pos
+ growVector
+ gravityVector
339 # Check for collisions with the object
340 climbing
= collision(ob
, prevIvy
.pos
, newPos
)
342 # Update the growing vector for any collisions
343 growVector
= newPos
- prevIvy
.pos
- gravityVector
344 growVector
.normalize()
346 # Create a new IvyNode and set its properties
348 tmpNode
.climb
= climbing
350 tmpNode
.primaryDir
= prevIvy
.primaryDir
.lerp(growVector
, 0.5)
351 tmpNode
.primaryDir
.normalize()
352 tmpNode
.adhesionVector
= adhesionVector
353 tmpNode
.length
= prevIvy
.length
+ (newPos
- prevIvy
.pos
).length
355 if tmpNode
.length
> self
.maxLength
:
356 self
.maxLength
= tmpNode
.length
358 # If the node isn't climbing, update it's floating length
359 # Otherwise set it to 0
361 tmpNode
.floatingLength
= prevIvy
.floatingLength
+ (newPos
-
364 tmpNode
.floatingLength
= 0.0
366 root
.ivyNodes
.append(tmpNode
)
368 # Loop through all roots to check if a new root is generated
369 for root
in self
.ivyRoots
:
370 # Check the root is alive and isn't at high level of recursion
371 if (root
.parents
> 3) or (not root
.alive
):
374 # Check to make sure there's more than 1 node
375 if len(root
.ivyNodes
) > 1:
376 # Loop through all nodes in root to check if new root is grown
377 for node
in root
.ivyNodes
:
378 # Set the last node of the root and find the weighting
379 prevIvy
= root
.ivyNodes
[-1]
380 weight
= 1.0 - (cos(2.0 * pi
* node
.length
/
381 prevIvy
.length
) * 0.5 + 0.5)
383 probability
= rand_val()
385 # Check if a new root is grown and if so, set its values
386 if (probability
* weight
> self
.branchingProbability
):
388 tmpNode
.pos
= node
.pos
389 tmpNode
.floatingLength
= node
.floatingLength
392 tmpRoot
.parents
= root
.parents
+ 1
394 tmpRoot
.ivyNodes
.append(tmpNode
)
395 self
.ivyRoots
.append(tmpRoot
)
399 def adhesion(loc
, ob
, max_l
):
400 # Get transfor vector and transformed loc
401 tran_mat
= ob
.matrix_world
.inverted()
402 tran_loc
= tran_mat
* loc
404 # Compute the adhesion vector by finding the nearest point
405 nearest_result
= ob
.closest_point_on_mesh(tran_loc
, max_l
)
406 adhesion_vector
= Vector((0.0, 0.0, 0.0))
407 if nearest_result
[0]:
408 # Compute the distance to the nearest point
409 adhesion_vector
= ob
.matrix_world
* nearest_result
[1] - loc
410 distance
= adhesion_vector
.length
411 # If it's less than the maximum allowed and not 0, continue
413 # Compute the direction vector between the closest point and loc
414 adhesion_vector
.normalize()
415 adhesion_vector
*= 1.0 - distance
/ max_l
416 # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3])
417 return adhesion_vector
420 def collision(ob
, pos
, new_pos
):
421 # Check for collision with the object
425 tran_mat
= ob
.matrix_world
.inverted()
426 tran_pos
= tran_mat
* pos
427 tran_new_pos
= tran_mat
* new_pos
428 tran_dir
= tran_new_pos
- tran_pos
430 ray_result
= ob
.ray_cast(tran_pos
, tran_dir
, tran_dir
.length
)
431 # If there's a collision we need to check it
433 # Check whether the collision is going into the object
434 if tran_dir
.dot(ray_result
[2]) < 0.0:
435 # Find projection of the piont onto the plane
436 p0
= tran_new_pos
- (tran_new_pos
-
437 ray_result
[1]).project(ray_result
[2])
438 # Reflect in the plane
439 tran_new_pos
+= 2 * (p0
- tran_new_pos
)
441 new_pos
+= ob
.matrix_world
* tran_new_pos
446 def check_mesh_faces(ob
):
448 if len(me
.polygons
) > 0:
454 class IvyGen(Operator
):
455 bl_idname
= "curve.ivy_gen"
457 bl_description
= "Generate Ivy on an Mesh Object"
458 bl_options
= {'REGISTER', 'UNDO'}
460 updateIvy
= BoolProperty(
462 description
="Update the Ivy location based on the cursor and Panel settings",
465 defaultIvy
= BoolProperty(
467 options
={"HIDDEN", "SKIP_SAVE"},
472 def poll(self
, context
):
473 # Check if there's an object and whether it's a mesh
474 ob
= context
.active_object
475 return ((ob
is not None) and
476 (ob
.type == 'MESH') and
477 (context
.mode
== 'OBJECT'))
479 def invoke(self
, context
, event
):
480 self
.updateIvy
= True
481 return self
.execute(context
)
483 def execute(self
, context
):
484 # scene = context.scene
485 ivyProps
= context
.window_manager
.ivy_gen_props
487 if not self
.updateIvy
:
488 return {'PASS_THROUGH'}
490 # assign the variables, check if it is default
491 # Note: update the values if window_manager props defaults are changed
492 randomSeed
= ivyProps
.randomSeed
if not self
.defaultIvy
else 0
493 maxTime
= ivyProps
.maxTime
if not self
.defaultIvy
else 0
494 maxIvyLength
= ivyProps
.maxIvyLength
if not self
.defaultIvy
else 1.0
495 ivySize
= ivyProps
.ivySize
if not self
.defaultIvy
else 0.02
496 maxFloatLength
= ivyProps
.maxFloatLength
if not self
.defaultIvy
else 0.5
497 maxAdhesionDistance
= ivyProps
.maxAdhesionDistance
if not self
.defaultIvy
else 1.0
498 primaryWeight
= ivyProps
.primaryWeight
if not self
.defaultIvy
else 0.5
499 randomWeight
= ivyProps
.randomWeight
if not self
.defaultIvy
else 0.2
500 gravityWeight
= ivyProps
.gravityWeight
if not self
.defaultIvy
else 1.0
501 adhesionWeight
= ivyProps
.adhesionWeight
if not self
.defaultIvy
else 0.1
502 branchingProbability
= ivyProps
.branchingProbability
if not self
.defaultIvy
else 0.05
503 leafProbability
= ivyProps
.leafProbability
if not self
.defaultIvy
else 0.35
504 ivyBranchSize
= ivyProps
.ivyBranchSize
if not self
.defaultIvy
else 0.001
505 ivyLeafSize
= ivyProps
.ivyLeafSize
if not self
.defaultIvy
else 0.02
506 growLeaves
= ivyProps
.growLeaves
if not self
.defaultIvy
else True
508 bpy
.ops
.object.mode_set(mode
='EDIT', toggle
=False)
509 bpy
.ops
.object.mode_set(mode
='OBJECT', toggle
=False)
511 # Get the selected object
512 ob
= context
.active_object
514 # Check if the mesh has at least one polygon since some functions
515 # are expecting them in the object's data (see T51753)
516 check_face
= check_mesh_faces(ob
)
517 if check_face
is False:
518 self
.report({'WARNING'},
519 "Mesh Object doesn't have at least one Face. "
520 "Operation Cancelled")
523 # Compute bounding sphere radius
524 # radius = computeBoundingSphere(ob) # Not needed anymore
526 # Get the seeding point
527 seedPoint
= context
.scene
.cursor_location
529 # Fix the random seed
530 rand_seed(randomSeed
)
534 primaryWeight
=primaryWeight
,
535 randomWeight
=randomWeight
,
536 gravityWeight
=gravityWeight
,
537 adhesionWeight
=adhesionWeight
,
538 branchingProbability
=branchingProbability
,
539 leafProbability
=leafProbability
,
541 ivyLeafSize
=ivyLeafSize
,
542 ivyBranchSize
=ivyBranchSize
,
543 maxFloatLength
=maxFloatLength
,
544 maxAdhesionDistance
=maxAdhesionDistance
546 # Generate first root and node
550 maxLength
= maxIvyLength
# * radius
552 # If we need to check time set the flag
558 checkAliveIter
= [True, ]
560 # Grow until 200 roots is reached or backup counter exceeds limit
561 while (any(checkAliveIter
) and
562 (IVY
.maxLength
< maxLength
) and
563 (not checkTime
or (time
.time() - t
< maxTime
))):
564 # Grow the ivy for this iteration
567 # Print the proportion of ivy growth to console
568 if (IVY
.maxLength
/ maxLength
* 100) > 10 * startPercent
// 10:
569 print('%0.2f%% of Ivy nodes have grown' %
570 (IVY
.maxLength
/ maxLength
* 100))
572 if IVY
.maxLength
/ maxLength
> 1:
573 print("Halting Growth")
575 # Make an iterator to check if all are alive
576 checkAliveIter
= (r
.alive
for r
in IVY
.ivyRoots
)
578 # Create the curve and leaf geometry
579 createIvyGeometry(IVY
, growLeaves
)
580 print("Geometry Generation Complete")
582 print("Ivy generated in %0.2f s" % (time
.time() - t
))
584 self
.updateIvy
= False
585 self
.defaultIvy
= False
589 def draw(self
, context
):
592 layout
.prop(self
, "updateIvy", icon
="FILE_REFRESH")
595 class CURVE_PT_IvyGenPanel(Panel
):
596 bl_label
= "Ivy Generator"
597 bl_idname
= "CURVE_PT_IvyGenPanel"
598 bl_space_type
= "VIEW_3D"
599 bl_region_type
= "TOOLS"
600 bl_category
= "Create"
601 bl_context
= 'objectmode'
602 bl_options
= {"DEFAULT_CLOSED"}
604 def draw(self
, context
):
606 wm
= context
.window_manager
607 col
= layout
.column(align
=True)
609 prop_new
= col
.operator("curve.ivy_gen", text
="Add New Ivy", icon
="OUTLINER_OB_CURVE")
610 prop_new
.defaultIvy
= False
611 prop_new
.updateIvy
= True
613 prop_def
= col
.operator("curve.ivy_gen", text
="Add New Default Ivy", icon
="CURVE_DATA")
614 prop_def
.defaultIvy
= True
615 prop_def
.updateIvy
= True
617 col
= layout
.column(align
=True)
618 col
.label("Generation Settings:")
619 col
.prop(wm
.ivy_gen_props
, "randomSeed")
620 col
.prop(wm
.ivy_gen_props
, "maxTime")
622 col
= layout
.column(align
=True)
623 col
.label("Size Settings:")
624 col
.prop(wm
.ivy_gen_props
, "maxIvyLength")
625 col
.prop(wm
.ivy_gen_props
, "ivySize")
626 col
.prop(wm
.ivy_gen_props
, "maxFloatLength")
627 col
.prop(wm
.ivy_gen_props
, "maxAdhesionDistance")
629 col
= layout
.column(align
=True)
630 col
.label("Weight Settings:")
631 col
.prop(wm
.ivy_gen_props
, "primaryWeight")
632 col
.prop(wm
.ivy_gen_props
, "randomWeight")
633 col
.prop(wm
.ivy_gen_props
, "gravityWeight")
634 col
.prop(wm
.ivy_gen_props
, "adhesionWeight")
636 col
= layout
.column(align
=True)
637 col
.label("Branch Settings:")
638 col
.prop(wm
.ivy_gen_props
, "branchingProbability")
639 col
.prop(wm
.ivy_gen_props
, "ivyBranchSize")
641 col
= layout
.column(align
=True)
642 col
.prop(wm
.ivy_gen_props
, "growLeaves")
644 if wm
.ivy_gen_props
.growLeaves
:
645 col
= layout
.column(align
=True)
646 col
.label("Leaf Settings:")
647 col
.prop(wm
.ivy_gen_props
, "ivyLeafSize")
648 col
.prop(wm
.ivy_gen_props
, "leafProbability")
651 class IvyGenProperties(PropertyGroup
):
652 maxIvyLength
= FloatProperty(
653 name
="Max Ivy Length",
654 description
="Maximum ivy length in Blender Units",
661 primaryWeight
= FloatProperty(
662 name
="Primary Weight",
663 description
="Weighting given to the current direction",
668 randomWeight
= FloatProperty(
669 name
="Random Weight",
670 description
="Weighting given to the random direction",
675 gravityWeight
= FloatProperty(
676 name
="Gravity Weight",
677 description
="Weighting given to the gravity direction",
682 adhesionWeight
= FloatProperty(
683 name
="Adhesion Weight",
684 description
="Weighting given to the adhesion direction",
689 branchingProbability
= FloatProperty(
690 name
="Branching Probability",
691 description
="Probability of a new branch forming",
696 leafProbability
= FloatProperty(
697 name
="Leaf Probability",
698 description
="Probability of a leaf forming",
703 ivySize
= FloatProperty(
705 description
="The length of an ivy segment in Blender"
712 ivyLeafSize
= FloatProperty(
713 name
="Ivy Leaf Size",
714 description
="The size of the ivy leaves",
720 ivyBranchSize
= FloatProperty(
721 name
="Ivy Branch Size",
722 description
="The size of the ivy branches",
728 maxFloatLength
= FloatProperty(
729 name
="Max Float Length",
730 description
="The maximum distance that a branch "
731 "can live while floating",
736 maxAdhesionDistance
= FloatProperty(
737 name
="Max Adhesion Length",
738 description
="The maximum distance that a branch "
739 "will feel the effects of adhesion",
745 randomSeed
= IntProperty(
747 description
="The seed governing random generation",
751 maxTime
= FloatProperty(
753 description
="The maximum time to run the generation for "
754 "in seconds generation (0.0 = Disabled)",
759 growLeaves
= BoolProperty(
761 description
="Grow leaves or not",
775 bpy
.utils
.register_class(cls
)
777 bpy
.types
.WindowManager
.ivy_gen_props
= PointerProperty(
778 type=IvyGenProperties
783 del bpy
.types
.WindowManager
.ivy_gen_props
785 for cls
in reversed(classes
):
786 bpy
.utils
.unregister_class(cls
)
789 if __name__
== "__main__":