Avoid writing redundant zeros
[blender-addons.git] / add_curve_ivygen.py
blobac457172a1787c3fd0795769bcb7b93ae3f68483
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[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
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[3])
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
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
430 if ray_result[0]:
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)
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()