align columns (for multi-drag)
[blender-addons.git] / add_curve_ivygen.py
blob6b359e1493d8618e19096cdeade7793a999e5647
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 "
28 "at the 3D cursor",
29 "warning": "",
30 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
31 "Scripts/Curve/Ivy_Gen",
32 "category": "Add Curve",
36 import bpy
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
42 import time
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'
54 curve.bevel_depth = 1
55 curve.fill_mode = 'FULL'
56 curve.resolution_u = 4
58 if growLeaves:
59 # Create the ivy leaves
60 # Order location of the vertices
61 signList = ((-1.0, +1.0),
62 (+1.0, +1.0),
63 (+1.0, -1.0),
64 (-1.0, -1.0),
67 # Get the local size
68 #local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize
70 # Initialise the vertex and face lists
71 vertList = deque()
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)
81 if numNodes > 1:
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 *
117 prevIvyLength, 2)
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
126 theta = (0.5 *
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
139 for j in range(10):
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,
148 rand_val() - 0.5,
149 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
173 k1, k2 in signList])
175 # Add the object and link to scene
176 newCurve = bpy.data.objects.new("IVY_Curve", curve)
177 bpy.context.scene.objects.link(newCurve)
179 if growLeaves:
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?
195 for d in tex.data:
196 uv1, uv2, uv3, uv4 = signList
199 ob.parent = newCurve
202 def computeBoundingSphere(ob):
203 # Get the mesh data
204 me = ob.data
205 # Intialise the center
206 center = Vector((0.0, 0.0, 0.0))
207 # Add all vertex coords
208 for v in me.vertices:
209 center += v.co
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)
215 return radius
219 class IvyNode:
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')
224 def __init__(self):
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))
229 self.length = 0.0001
230 self.floatingLength = 0.0
231 self.climb = True
234 class IvyRoot:
235 """ The class used to hold all ivy nodes growing from this root point."""
236 __slots__ = ('ivyNodes', 'alive', 'parents')
238 def __init__(self):
239 self.ivyNodes = deque()
240 self.alive = True
241 self.parents = 0
244 class Ivy:
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')
251 def __init__(self,
252 primaryWeight=0.5,
253 randomWeight=0.2,
254 gravityWeight=1.0,
255 adhesionWeight=0.1,
256 branchingProbability=0.05,
257 leafProbability=0.35,
258 ivySize=0.02,
259 ivyLeafSize=0.02,
260 ivyBranchSize=0.001,
261 maxFloatLength=0.5,
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
276 self.maxLength = 0.0
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
286 tmpRoot = IvyRoot()
287 tmpIvy = IvyNode()
288 tmpIvy.pos = seedPos
290 tmpRoot.ivyNodes.append(tmpIvy)
291 self.ivyRoots.append(tmpRoot)
293 def grow(self, ob):
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
301 if not root.alive:
302 continue
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:
309 root.alive = False
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 *
330 Vector((0, 0, -1)))
331 gravityVector *= pow(prevIvy.floatingLength / self.maxFloatLength,
332 0.7)
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
345 tmpNode = IvyNode()
346 tmpNode.climb = climbing
347 tmpNode.pos = newPos
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
358 if not climbing:
359 tmpNode.floatingLength = prevIvy.floatingLength + (newPos -
360 prevIvy.pos).length
361 else:
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):
370 continue
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):
385 tmpNode = IvyNode()
386 tmpNode.pos = node.pos
387 tmpNode.floatingLength = node.floatingLength
389 tmpRoot = IvyRoot()
390 tmpRoot.parents = root.parents + 1
392 tmpRoot.ivyNodes.append(tmpNode)
393 self.ivyRoots.append(tmpRoot)
394 return
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
410 if distance:
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
420 climbing = False
422 # Transform vecs
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)
437 new_pos *= 0
438 new_pos += ob.matrix_world * tran_new_pos
439 climbing = True
440 return climbing
443 class IvyGen(bpy.types.Operator):
444 bl_idname = "curve.ivy_gen"
445 bl_label = "IvyGen"
446 bl_options = {'REGISTER', 'UNDO'}
448 maxIvyLength = FloatProperty(name="Max Ivy Length",
449 description="Maximum ivy length in Blender Units",
450 default=1.0,
451 min=0.0,
452 soft_max=3.0,
453 subtype='DISTANCE',
454 unit='LENGTH')
455 primaryWeight = FloatProperty(name="Primary Weight",
456 description="Weighting given to the current direction",
457 default=0.5,
458 min=0.0,
459 soft_max=1.0)
460 randomWeight = FloatProperty(name="Random Weight",
461 description="Weighting given to the random direction",
462 default=0.2,
463 min=0.0,
464 soft_max=1.0)
465 gravityWeight = FloatProperty(name="Gravity Weight",
466 description="Weighting given to the gravity direction",
467 default=1.0,
468 min=0.0,
469 soft_max=1.0)
470 adhesionWeight = FloatProperty(name="Adhesion Weight",
471 description="Weighting given to the adhesion direction",
472 default=0.1,
473 min=0.0,
474 soft_max=1.0)
475 branchingProbability = FloatProperty(name="Branching Probability",
476 description="Probability of a new branch forming",
477 default=0.05,
478 min=0.0,
479 soft_max=1.0)
480 leafProbability = FloatProperty(name="Leaf Probability",
481 description="Probability of a leaf forming",
482 default=0.35,
483 min=0.0,
484 soft_max=1.0)
485 ivySize = FloatProperty(name="Ivy Size",
486 description=("The length of an ivy segment in Blender"
487 " Units"),
488 default=0.02,
489 min=0.0,
490 soft_max=1.0,
491 precision=3)
492 ivyLeafSize = FloatProperty(name="Ivy Leaf Size",
493 description="The size of the ivy leaves",
494 default=0.02,
495 min=0.0,
496 soft_max=0.5,
497 precision=3)
498 ivyBranchSize = FloatProperty(name="Ivy Branch Size",
499 description="The size of the ivy branches",
500 default=0.001,
501 min=0.0,
502 soft_max=0.1,
503 precision=4)
504 maxFloatLength = FloatProperty(name="Max Float Length",
505 description=("The maximum distance that a branch "
506 "can live while floating"),
507 default=0.5,
508 min=0.0,
509 soft_max=1.0)
510 maxAdhesionDistance = FloatProperty(name="Max Adhesion Length",
511 description=("The maximum distance that a branch "
512 "will feel the effects of adhesion"),
513 default=1.0,
514 min=0.0,
515 soft_max=2.0,
516 precision=2)
517 randomSeed = IntProperty(name="Random Seed",
518 description="The seed governing random generation",
519 default=0,
520 min=0)
521 maxTime = FloatProperty(name="Maximum Time",
522 description=("The maximum time to run the generation for "
523 "in seconds generation (0.0 = Disabled)"),
524 default=0.0,
525 min=0.0,
526 soft_max=10)
527 growLeaves = BoolProperty(name="Grow Leaves",
528 description="Grow leaves or not",
529 default=True)
530 updateIvy = BoolProperty(name="Update Ivy", default=False)
532 @classmethod
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)
559 # Make the new ivy
560 IVY = Ivy(**self.as_keywords(ignore=('randomSeed', 'growLeaves',
561 'maxIvyLength', 'maxTime', 'updateIvy')))
563 # Generate first root and node
564 IVY.seed(seedPoint)
566 checkTime = False
567 maxLength = self.maxIvyLength # * radius
569 # If we need to check time set the flag
570 if self.maxTime != 0.0:
571 checkTime = True
573 t = time.time()
574 startPercent = 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
582 IVY.grow(ob)
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))
588 startPercent += 10
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
603 return {'FINISHED'}
605 def draw(self, context):
606 layout = self.layout
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')
632 box = layout.box()
633 box.label("Generation Settings:")
634 box.prop(self, 'randomSeed')
635 box.prop(self, 'maxTime')
637 box = layout.box()
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')
644 box = layout.box()
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')
651 box = layout.box()
652 box.label("Branch Settings:")
653 box.prop(self, 'branchingProbability')
654 box.prop(self, 'ivyBranchSize')
656 if self.growLeaves:
657 box = layout.box()
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
668 def register():
669 bpy.utils.register_module(__name__)
670 bpy.types.INFO_MT_curve_add.append(menu_func)
673 def unregister():
674 bpy.types.INFO_MT_curve_add.remove(menu_func)
675 bpy.utils.unregister_module(__name__)
678 if __name__ == "__main__":
679 register()