set property to native line ending
[blender-addons.git] / add_curve_ivygen.py
blobaade98a7f0f13d36c446e8e1ed29f430a4aa44a0
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 #####
19 # <pep8-80 compliant>
21 bl_info = {
22 "name": "IvyGen",
23 "author": "testscreenings, PKHG, TrumanBlending",
24 "version": (0, 1, 1),
25 "blender": (2, 59, 0),
26 "location": "View3D > Add > Curve",
27 "description": "Adds generated ivy to a mesh object starting at the 3D"\
28 " cursor",
29 "warning": "",
30 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"\
31 "Scripts/Curve/Ivy_Gen",
32 "tracker_url": "http://projects.blender.org/tracker/index.php?"\
33 "func=detail&aid=27234",
34 "category": "Add Curve"}
37 import bpy
38 from bpy.props import FloatProperty, IntProperty, BoolProperty
39 from mathutils import Vector, Matrix
40 from collections import deque
41 from math import pow, cos, pi, atan2
42 from random import random as rand_val, seed as rand_seed
43 import time
46 def createIvyGeometry(IVY, growLeaves):
47 """Create the curve geometry for IVY"""
48 # Compute the local size and the gauss weight filter
49 #local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize
50 gaussWeight = (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0)
52 # Create a new curve and intialise it
53 curve = bpy.data.curves.new("IVY", type='CURVE')
54 curve.dimensions = '3D'
55 curve.bevel_depth = 1
56 curve.fill_mode = 'FULL'
57 curve.resolution_u = 4
59 if growLeaves:
60 # Create the ivy leaves
61 # Order location of the vertices
62 signList = ((-1.0, +1.0),
63 (+1.0, +1.0),
64 (+1.0, -1.0),
65 (-1.0, -1.0),
68 # Get the local size
69 #local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
71 # Initialise the vertex and face lists
72 vertList = deque()
74 # Store the methods for faster calling
75 addV = vertList.extend
76 rotMat = Matrix.Rotation
78 # Loop over all roots to generate its nodes
79 for root in IVY.ivyRoots:
80 # Only grow if more than one node
81 numNodes = len(root.ivyNodes)
82 if numNodes > 1:
83 # Calculate the local radius
84 local_ivyBranchRadius = 1.0 / (root.parents + 1) + 1.0
85 prevIvyLength = 1.0 / root.ivyNodes[-1].length
86 splineVerts = [ax for n in root.ivyNodes for ax in n.pos.to_4d()]
88 radiusConstant = local_ivyBranchRadius * IVY.ivyBranchSize
89 splineRadii = [radiusConstant * (1.3 - n.length * prevIvyLength)
90 for n in root.ivyNodes]
92 # Add the poly curve and set coords and radii
93 newSpline = curve.splines.new(type='POLY')
94 newSpline.points.add(len(splineVerts) // 4 - 1)
95 newSpline.points.foreach_set('co', splineVerts)
96 newSpline.points.foreach_set('radius', splineRadii)
98 # Loop over all nodes in the root
99 for i, n in enumerate(root.ivyNodes):
100 for k in range(len(gaussWeight)):
101 idx = max(0, min(i + k - 5, numNodes - 1))
102 n.smoothAdhesionVector += (gaussWeight[k] *
103 root.ivyNodes[idx].adhesionVector)
104 n.smoothAdhesionVector /= 56.0
105 n.adhesionLength = n.smoothAdhesionVector.length
106 n.smoothAdhesionVector.normalize()
108 if growLeaves and (i < numNodes - 1):
109 node = root.ivyNodes[i]
110 nodeNext = root.ivyNodes[i + 1]
112 # Find the weight and normalize the smooth adhesion vector
113 weight = pow(node.length * prevIvyLength, 0.7)
115 # Calculate the ground ivy and the new weight
116 groundIvy = max(0.0, -node.smoothAdhesionVector.z)
117 weight += groundIvy * pow(1 - node.length *
118 prevIvyLength, 2)
120 # Find the alignment weight
121 alignmentWeight = node.adhesionLength
123 # Calculate the needed angles
124 phi = atan2(node.smoothAdhesionVector.y,
125 node.smoothAdhesionVector.x) - pi / 2.0
127 theta = (0.5 *
128 node.smoothAdhesionVector.angle(Vector((0, 0, -1)), 0))
130 # Find the size weight
131 sizeWeight = 1.5 - (cos(2 * pi * weight) * 0.5 + 0.5)
133 # Randomise the angles
134 phi += (rand_val() - 0.5) * (1.3 - alignmentWeight)
135 theta += (rand_val() - 0.5) * (1.1 - alignmentWeight)
137 # Calculate the leaf size an append the face to the list
138 leafSize = IVY.ivyLeafSize * sizeWeight
140 for j in range(10):
141 # Generate the probability
142 probability = rand_val()
144 # If we need to grow a leaf, do so
145 if (probability * weight) > IVY.leafProbability:
147 # Generate the random vector
148 randomVector = Vector((rand_val() - 0.5,
149 rand_val() - 0.5,
150 rand_val() - 0.5,
153 # Find the leaf center
154 center = (node.pos.lerp(nodeNext.pos, j / 10.0) +
155 IVY.ivyLeafSize * randomVector)
157 # For each of the verts, rotate/scale and append
158 basisVecX = Vector((1, 0, 0))
159 basisVecY = Vector((0, 1, 0))
161 horiRot = rotMat(theta, 3, 'X')
162 vertRot = rotMat(phi, 3, 'Z')
164 basisVecX.rotate(horiRot)
165 basisVecY.rotate(horiRot)
167 basisVecX.rotate(vertRot)
168 basisVecY.rotate(vertRot)
170 basisVecX *= leafSize
171 basisVecY *= leafSize
173 addV([k1 * basisVecX + k2 * basisVecY + center for
174 k1, k2 in signList])
176 # Add the object and link to scene
177 newCurve = bpy.data.objects.new("IVY_Curve", curve)
178 bpy.context.scene.objects.link(newCurve)
180 if growLeaves:
181 faceList = [[4 * i + l for l in range(4)] for i in
182 range(len(vertList) // 4)]
184 # Generate the new leaf mesh and link
185 me = bpy.data.meshes.new('IvyLeaf')
186 me.from_pydata(vertList, [], faceList)
187 me.update(calc_edges=True)
188 ob = bpy.data.objects.new('IvyLeaf', me)
189 bpy.context.scene.objects.link(ob)
191 me.uv_textures.new("Leaves")
193 # Set the uv texture coords
194 # TODO, this is non-functional, default uvs are ok?
196 for d in tex.data:
197 uv1, uv2, uv3, uv4 = signList
200 ob.parent = newCurve
203 def computeBoundingSphere(ob):
204 # Get the mesh data
205 me = ob.data
206 # Intialise the center
207 center = Vector((0.0, 0.0, 0.0))
208 # Add all vertex coords
209 for v in me.vertices:
210 center += v.co
211 # Average over all verts
212 center /= len(me.vertices)
213 # Create the iterator and find its max
214 length_iter = ((center - v.co).length for v in me.vertices)
215 radius = max(length_iter)
216 return radius
220 class IvyNode:
221 """ The basic class used for each point on the ivy which is grown."""
222 __slots__ = ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength',
223 'smoothAdhesionVector', 'length', 'floatingLength', 'climb')
225 def __init__(self):
226 self.pos = Vector((0, 0, 0))
227 self.primaryDir = Vector((0, 0, 1))
228 self.adhesionVector = Vector((0, 0, 0))
229 self.smoothAdhesionVector = Vector((0, 0, 0))
230 self.length = 0.0001
231 self.floatingLength = 0.0
232 self.climb = True
235 class IvyRoot:
236 """ The class used to hold all ivy nodes growing from this root point."""
237 __slots__ = ('ivyNodes', 'alive', 'parents')
239 def __init__(self):
240 self.ivyNodes = deque()
241 self.alive = True
242 self.parents = 0
245 class Ivy:
246 """ The class holding all parameters and ivy roots."""
247 __slots__ = ('ivyRoots', 'primaryWeight', 'randomWeight',
248 'gravityWeight', 'adhesionWeight', 'branchingProbability',
249 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize',
250 'maxFloatLength', 'maxAdhesionDistance', 'maxLength')
252 def __init__(self,
253 primaryWeight=0.5,
254 randomWeight=0.2,
255 gravityWeight=1.0,
256 adhesionWeight=0.1,
257 branchingProbability=0.05,
258 leafProbability=0.35,
259 ivySize=0.02,
260 ivyLeafSize=0.02,
261 ivyBranchSize=0.001,
262 maxFloatLength=0.5,
263 maxAdhesionDistance=1.0):
265 self.ivyRoots = deque()
266 self.primaryWeight = primaryWeight
267 self.randomWeight = randomWeight
268 self.gravityWeight = gravityWeight
269 self.adhesionWeight = adhesionWeight
270 self.branchingProbability = 1 - branchingProbability
271 self.leafProbability = 1 - leafProbability
272 self.ivySize = ivySize
273 self.ivyLeafSize = ivyLeafSize
274 self.ivyBranchSize = ivyBranchSize
275 self.maxFloatLength = maxFloatLength
276 self.maxAdhesionDistance = maxAdhesionDistance
277 self.maxLength = 0.0
279 # Normalize all the weights only on intialisation
280 sum = self.primaryWeight + self.randomWeight + self.adhesionWeight
281 self.primaryWeight /= sum
282 self.randomWeight /= sum
283 self.adhesionWeight /= sum
285 def seed(self, seedPos):
286 # Seed the Ivy by making a new root and first node
287 tmpRoot = IvyRoot()
288 tmpIvy = IvyNode()
289 tmpIvy.pos = seedPos
291 tmpRoot.ivyNodes.append(tmpIvy)
292 self.ivyRoots.append(tmpRoot)
294 def grow(self, ob):
295 # Determine the local sizes
296 #local_ivySize = self.ivySize # * radius
297 #local_maxFloatLength = self.maxFloatLength # * radius
298 #local_maxAdhesionDistance = self.maxAdhesionDistance # * radius
300 for root in self.ivyRoots:
301 # Make sure the root is alive, if not, skip
302 if not root.alive:
303 continue
305 # Get the last node in the current root
306 prevIvy = root.ivyNodes[-1]
308 # If the node is floating for too long, kill the root
309 if prevIvy.floatingLength > self.maxFloatLength:
310 root.alive = False
312 # Set the primary direction from the last node
313 primaryVector = prevIvy.primaryDir
315 # Make the random vector and normalize
316 randomVector = Vector((rand_val() - 0.5, rand_val() - 0.5,
317 rand_val() - 0.5)) + Vector((0, 0, 0.2))
318 randomVector.normalize()
320 # Calculate the adhesion vector
321 adhesionVector = adhesion(prevIvy.pos, ob,
322 self.maxAdhesionDistance)
324 # Calculate the growing vector
325 growVector = self.ivySize * (primaryVector * self.primaryWeight +
326 randomVector * self.randomWeight +
327 adhesionVector * self.adhesionWeight)
329 # Find the gravity vector
330 gravityVector = (self.ivySize * self.gravityWeight *
331 Vector((0, 0, -1)))
332 gravityVector *= pow(prevIvy.floatingLength / self.maxFloatLength,
333 0.7)
335 # Determine the new position vector
336 newPos = prevIvy.pos + growVector + gravityVector
338 # Check for collisions with the object
339 climbing = collision(ob, prevIvy.pos, newPos)
341 # Update the growing vector for any collisions
342 growVector = newPos - prevIvy.pos - gravityVector
343 growVector.normalize()
345 # Create a new IvyNode and set its properties
346 tmpNode = IvyNode()
347 tmpNode.climb = climbing
348 tmpNode.pos = newPos
349 tmpNode.primaryDir = prevIvy.primaryDir.lerp(growVector, 0.5)
350 tmpNode.primaryDir.normalize()
351 tmpNode.adhesionVector = adhesionVector
352 tmpNode.length = prevIvy.length + (newPos - prevIvy.pos).length
354 if tmpNode.length > self.maxLength:
355 self.maxLength = tmpNode.length
357 # If the node isn't climbing, update it's floating length
358 # Otherwise set it to 0
359 if not climbing:
360 tmpNode.floatingLength = prevIvy.floatingLength + (newPos -
361 prevIvy.pos).length
362 else:
363 tmpNode.floatingLength = 0.0
365 root.ivyNodes.append(tmpNode)
367 # Loop through all roots to check if a new root is generated
368 for root in self.ivyRoots:
369 # Check the root is alive and isn't at high level of recursion
370 if (root.parents > 3) or (not root.alive):
371 continue
373 # Check to make sure there's more than 1 node
374 if len(root.ivyNodes) > 1:
375 # Loop through all nodes in root to check if new root is grown
376 for node in root.ivyNodes:
377 # Set the last node of the root and find the weighting
378 prevIvy = root.ivyNodes[-1]
379 weight = 1.0 - (cos(2.0 * pi * node.length /
380 prevIvy.length) * 0.5 + 0.5)
382 probability = rand_val()
384 # Check if a new root is grown and if so, set its values
385 if (probability * weight > self.branchingProbability):
386 tmpNode = IvyNode()
387 tmpNode.pos = node.pos
388 tmpNode.floatingLength = node.floatingLength
390 tmpRoot = IvyRoot()
391 tmpRoot.parents = root.parents + 1
393 tmpRoot.ivyNodes.append(tmpNode)
394 self.ivyRoots.append(tmpRoot)
395 return
398 def adhesion(loc, ob, max_l):
399 # Get transfor vector and transformed loc
400 tran_mat = ob.matrix_world.inverted()
401 tran_loc = tran_mat * loc
403 # Compute the adhesion vector by finding the nearest point
404 nearest_result = ob.closest_point_on_mesh(tran_loc, max_l)
405 adhesion_vector = Vector((0.0, 0.0, 0.0))
406 if nearest_result[2] != -1:
407 # Compute the distance to the nearest point
408 adhesion_vector = ob.matrix_world * nearest_result[0] - loc
409 distance = adhesion_vector.length
410 # If it's less than the maximum allowed and not 0, continue
411 if distance:
412 # Compute the direction vector between the closest point and loc
413 adhesion_vector.normalize()
414 adhesion_vector *= 1.0 - distance / max_l
415 #adhesion_vector *= getFaceWeight(ob.data, nearest_result[2])
416 return adhesion_vector
419 def collision(ob, pos, new_pos):
420 # Check for collision with the object
421 climbing = False
423 # Transform vecs
424 tran_mat = ob.matrix_world.inverted()
425 tran_pos = tran_mat * pos
426 tran_new_pos = tran_mat * new_pos
428 ray_result = ob.ray_cast(tran_pos, tran_new_pos)
429 # If there's a collision we need to check it
430 if ray_result[2] != -1:
431 # Check whether the collision is going into the object
432 if (tran_new_pos - tran_pos).dot(ray_result[1]) < 0.0:
433 # Find projection of the piont onto the plane
434 p0 = tran_new_pos - (tran_new_pos -
435 ray_result[0]).project(ray_result[1])
436 # Reflect in the plane
437 tran_new_pos += 2 * (p0 - tran_new_pos)
438 new_pos *= 0
439 new_pos += ob.matrix_world * tran_new_pos
440 climbing = True
441 return climbing
444 class IvyGen(bpy.types.Operator):
445 bl_idname = "curve.ivy_gen"
446 bl_label = "IvyGen"
447 bl_options = {'REGISTER', 'UNDO'}
449 maxIvyLength = FloatProperty(name="Max Ivy Length",
450 description="Maximum ivy length in Blender Units",
451 default=1.0,
452 min=0.0,
453 soft_max=3.0,
454 subtype='DISTANCE',
455 unit='LENGTH')
456 primaryWeight = FloatProperty(name="Primary Weight",
457 description="Weighting given to the current direction",
458 default=0.5,
459 min=0.0,
460 soft_max=1.0)
461 randomWeight = FloatProperty(name="Random Weight",
462 description="Weighting given to the random direction",
463 default=0.2,
464 min=0.0,
465 soft_max=1.0)
466 gravityWeight = FloatProperty(name="Gravity Weight",
467 description="Weighting given to the gravity direction",
468 default=1.0,
469 min=0.0,
470 soft_max=1.0)
471 adhesionWeight = FloatProperty(name="Adhesion Weight",
472 description="Weighting given to the adhesion direction",
473 default=0.1,
474 min=0.0,
475 soft_max=1.0)
476 branchingProbability = FloatProperty(name="Branching Probability",
477 description="Probability of a new branch forming",
478 default=0.05,
479 min=0.0,
480 soft_max=1.0)
481 leafProbability = FloatProperty(name="Leaf Probability",
482 description="Probability of a leaf forming",
483 default=0.35,
484 min=0.0,
485 soft_max=1.0)
486 ivySize = FloatProperty(name="Ivy Size",
487 description=("The length of an ivy segment in Blender"
488 " Units"),
489 default=0.02,
490 min=0.0,
491 soft_max=1.0,
492 precision=3)
493 ivyLeafSize = FloatProperty(name="Ivy Leaf Size",
494 description="The size of the ivy leaves",
495 default=0.02,
496 min=0.0,
497 soft_max=0.5,
498 precision=3)
499 ivyBranchSize = FloatProperty(name="Ivy Branch Size",
500 description="The size of the ivy branches",
501 default=0.001,
502 min=0.0,
503 soft_max=0.1,
504 precision=4)
505 maxFloatLength = FloatProperty(name="Max Float Length",
506 description=("The maximum distance that a branch "
507 "can live while floating"),
508 default=0.5,
509 min=0.0,
510 soft_max=1.0)
511 maxAdhesionDistance = FloatProperty(name="Max Adhesion Length",
512 description=("The maximum distance that a branch "
513 "will feel the effects of adhesion"),
514 default=1.0,
515 min=0.0,
516 soft_max=2.0,
517 precision=2)
518 randomSeed = IntProperty(name="Random Seed",
519 description="The seed governing random generation",
520 default=0,
521 min=0)
522 maxTime = FloatProperty(name="Maximum Time",
523 description=("The maximum time to run the generation for "
524 "in seconds generation (0.0 = Disabled)"),
525 default=0.0,
526 min=0.0,
527 soft_max=10)
528 growLeaves = BoolProperty(name="Grow Leaves",
529 description="Grow leaves or not",
530 default=True)
531 updateIvy = BoolProperty(name="Update Ivy", default=False)
533 @classmethod
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)
560 # Make the new ivy
561 IVY = Ivy(**self.as_keywords(ignore=('randomSeed', 'growLeaves',
562 'maxIvyLength', 'maxTime', 'updateIvy')))
564 # Generate first root and node
565 IVY.seed(seedPoint)
567 checkTime = False
568 maxLength = self.maxIvyLength # * radius
570 # If we need to check time set the flag
571 if self.maxTime != 0.0:
572 checkTime = True
574 t = time.time()
575 startPercent = 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
583 IVY.grow(ob)
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))
589 startPercent += 10
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
604 return {'FINISHED'}
606 def draw(self, context):
607 layout = self.layout
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')
633 box = layout.box()
634 box.label("Generation Settings:")
635 box.prop(self, 'randomSeed')
636 box.prop(self, 'maxTime')
638 box = layout.box()
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')
645 box = layout.box()
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')
652 box = layout.box()
653 box.label("Branch Settings:")
654 box.prop(self, 'branchingProbability')
655 box.prop(self, 'ivyBranchSize')
657 if self.growLeaves:
658 box = layout.box()
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
669 def register():
670 bpy.utils.register_module(__name__)
671 bpy.types.INFO_MT_curve_add.append(menu_func)
674 def unregister():
675 bpy.types.INFO_MT_curve_add.remove(menu_func)
676 bpy.utils.unregister_module(__name__)
679 if __name__ == "__main__":
680 register()