# SPDX-License-Identifier: GPL-2.0-or-later bl_info = { "name": "IvyGen", "author": "testscreenings, PKHG, TrumanBlending", "version": (0, 1, 5), "blender": (2, 80, 0), "location": "View3D > Sidebar > Ivy Generator (Create Tab)", "description": "Adds generated ivy to a mesh object starting " "at the 3D cursor", "warning": "", "doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/ivy_gen.html", "category": "Add Curve", } import bpy from bpy.types import ( Operator, Panel, PropertyGroup, ) from bpy.props import ( BoolProperty, FloatProperty, IntProperty, PointerProperty, ) from mathutils.bvhtree import BVHTree from mathutils import ( Vector, Matrix, ) from collections import deque from math import ( pow, cos, pi, atan2, ) from random import ( random as rand_val, seed as rand_seed, ) import time def createIvyGeometry(IVY, growLeaves): """Create the curve geometry for IVY""" # Compute the local size and the gauss weight filter # local_ivyBranchSize = IVY.ivyBranchSize # * radius * IVY.ivySize gaussWeight = (1.0, 2.0, 4.0, 7.0, 9.0, 10.0, 9.0, 7.0, 4.0, 2.0, 1.0) # Create a new curve and initialise it curve = bpy.data.curves.new("IVY", type='CURVE') curve.dimensions = '3D' curve.bevel_depth = 1 curve.fill_mode = 'FULL' curve.resolution_u = 4 if growLeaves: # Create the ivy leaves # Order location of the vertices signList = ((-1.0, +1.0), (+1.0, +1.0), (+1.0, -1.0), (-1.0, -1.0), ) # Get the local size # local_ivyLeafSize = IVY.ivyLeafSize # * radius * IVY.ivySize # Initialise the vertex and face lists vertList = deque() # Store the methods for faster calling addV = vertList.extend rotMat = Matrix.Rotation # Loop over all roots to generate its nodes for root in IVY.ivyRoots: # Only grow if more than one node numNodes = len(root.ivyNodes) if numNodes > 1: # Calculate the local radius local_ivyBranchRadius = 1.0 / (root.parents + 1) + 1.0 prevIvyLength = 1.0 / root.ivyNodes[-1].length splineVerts = [ax for n in root.ivyNodes for ax in n.pos.to_4d()] radiusConstant = local_ivyBranchRadius * IVY.ivyBranchSize splineRadii = [radiusConstant * (1.3 - n.length * prevIvyLength) for n in root.ivyNodes] # Add the poly curve and set coords and radii newSpline = curve.splines.new(type='POLY') newSpline.points.add(len(splineVerts) // 4 - 1) newSpline.points.foreach_set('co', splineVerts) newSpline.points.foreach_set('radius', splineRadii) # Loop over all nodes in the root for i, n in enumerate(root.ivyNodes): for k in range(len(gaussWeight)): idx = max(0, min(i + k - 5, numNodes - 1)) n.smoothAdhesionVector += (gaussWeight[k] * root.ivyNodes[idx].adhesionVector) n.smoothAdhesionVector /= 56.0 n.adhesionLength = n.smoothAdhesionVector.length n.smoothAdhesionVector.normalize() if growLeaves and (i < numNodes - 1): node = root.ivyNodes[i] nodeNext = root.ivyNodes[i + 1] # Find the weight and normalize the smooth adhesion vector weight = pow(node.length * prevIvyLength, 0.7) # Calculate the ground ivy and the new weight groundIvy = max(0.0, -node.smoothAdhesionVector.z) weight += groundIvy * pow(1 - node.length * prevIvyLength, 2) # Find the alignment weight alignmentWeight = node.adhesionLength # Calculate the needed angles phi = atan2(node.smoothAdhesionVector.y, node.smoothAdhesionVector.x) - pi / 2.0 theta = (0.5 * node.smoothAdhesionVector.angle(Vector((0, 0, -1)), 0)) # Find the size weight sizeWeight = 1.5 - (cos(2 * pi * weight) * 0.5 + 0.5) # Randomise the angles phi += (rand_val() - 0.5) * (1.3 - alignmentWeight) theta += (rand_val() - 0.5) * (1.1 - alignmentWeight) # Calculate the leaf size an append the face to the list leafSize = IVY.ivyLeafSize * sizeWeight for j in range(10): # Generate the probability probability = rand_val() # If we need to grow a leaf, do so if (probability * weight) > IVY.leafProbability: # Generate the random vector randomVector = Vector((rand_val() - 0.5, rand_val() - 0.5, rand_val() - 0.5, )) # Find the leaf center center = (node.pos.lerp(nodeNext.pos, j / 10.0) + IVY.ivyLeafSize * randomVector) # For each of the verts, rotate/scale and append basisVecX = Vector((1, 0, 0)) basisVecY = Vector((0, 1, 0)) horiRot = rotMat(theta, 3, 'X') vertRot = rotMat(phi, 3, 'Z') basisVecX.rotate(horiRot) basisVecY.rotate(horiRot) basisVecX.rotate(vertRot) basisVecY.rotate(vertRot) basisVecX *= leafSize basisVecY *= leafSize addV([k1 * basisVecX + k2 * basisVecY + center for k1, k2 in signList]) # Add the object and link to scene newCurve = bpy.data.objects.new("IVY_Curve", curve) bpy.context.collection.objects.link(newCurve) if growLeaves: faceList = [[4 * i + l for l in range(4)] for i in range(len(vertList) // 4)] # Generate the new leaf mesh and link me = bpy.data.meshes.new('IvyLeaf') me.from_pydata(vertList, [], faceList) me.update(calc_edges=True) ob = bpy.data.objects.new('IvyLeaf', me) bpy.context.collection.objects.link(ob) me.uv_layers.new(name="Leaves") # Set the uv texture coords # TODO, this is non-functional, default uvs are ok? ''' for d in tex.data: uv1, uv2, uv3, uv4 = signList ''' ob.parent = newCurve class IvyNode: """ The basic class used for each point on the ivy which is grown.""" __slots__ = ('pos', 'primaryDir', 'adhesionVector', 'adhesionLength', 'smoothAdhesionVector', 'length', 'floatingLength', 'climb') def __init__(self): self.pos = Vector((0, 0, 0)) self.primaryDir = Vector((0, 0, 1)) self.adhesionVector = Vector((0, 0, 0)) self.smoothAdhesionVector = Vector((0, 0, 0)) self.length = 0.0001 self.floatingLength = 0.0 self.climb = True class IvyRoot: """ The class used to hold all ivy nodes growing from this root point.""" __slots__ = ('ivyNodes', 'alive', 'parents') def __init__(self): self.ivyNodes = deque() self.alive = True self.parents = 0 class Ivy: """ The class holding all parameters and ivy roots.""" __slots__ = ('ivyRoots', 'primaryWeight', 'randomWeight', 'gravityWeight', 'adhesionWeight', 'branchingProbability', 'leafProbability', 'ivySize', 'ivyLeafSize', 'ivyBranchSize', 'maxFloatLength', 'maxAdhesionDistance', 'maxLength') def __init__(self, primaryWeight=0.5, randomWeight=0.2, gravityWeight=1.0, adhesionWeight=0.1, branchingProbability=0.05, leafProbability=0.35, ivySize=0.02, ivyLeafSize=0.02, ivyBranchSize=0.001, maxFloatLength=0.5, maxAdhesionDistance=1.0): self.ivyRoots = deque() self.primaryWeight = primaryWeight self.randomWeight = randomWeight self.gravityWeight = gravityWeight self.adhesionWeight = adhesionWeight self.branchingProbability = 1 - branchingProbability self.leafProbability = 1 - leafProbability self.ivySize = ivySize self.ivyLeafSize = ivyLeafSize self.ivyBranchSize = ivyBranchSize self.maxFloatLength = maxFloatLength self.maxAdhesionDistance = maxAdhesionDistance self.maxLength = 0.0 # Normalize all the weights only on initialisation sums = self.primaryWeight + self.randomWeight + self.adhesionWeight self.primaryWeight /= sums self.randomWeight /= sums self.adhesionWeight /= sums def seed(self, seedPos): # Seed the Ivy by making a new root and first node tmpRoot = IvyRoot() tmpIvy = IvyNode() tmpIvy.pos = seedPos tmpRoot.ivyNodes.append(tmpIvy) self.ivyRoots.append(tmpRoot) def grow(self, ob, bvhtree): # Determine the local sizes # local_ivySize = self.ivySize # * radius # local_maxFloatLength = self.maxFloatLength # * radius # local_maxAdhesionDistance = self.maxAdhesionDistance # * radius for root in self.ivyRoots: # Make sure the root is alive, if not, skip if not root.alive: continue # Get the last node in the current root prevIvy = root.ivyNodes[-1] # If the node is floating for too long, kill the root if prevIvy.floatingLength > self.maxFloatLength: root.alive = False # Set the primary direction from the last node primaryVector = prevIvy.primaryDir # Make the random vector and normalize randomVector = Vector((rand_val() - 0.5, rand_val() - 0.5, rand_val() - 0.5)) + Vector((0, 0, 0.2)) randomVector.normalize() # Calculate the adhesion vector adhesionVector = adhesion( prevIvy.pos, bvhtree, self.maxAdhesionDistance) # Calculate the growing vector growVector = self.ivySize * (primaryVector * self.primaryWeight + randomVector * self.randomWeight + adhesionVector * self.adhesionWeight) # Find the gravity vector gravityVector = (self.ivySize * self.gravityWeight * Vector((0, 0, -1))) gravityVector *= pow(prevIvy.floatingLength / self.maxFloatLength, 0.7) # Determine the new position vector newPos = prevIvy.pos + growVector + gravityVector # Check for collisions with the object climbing, newPos = collision(bvhtree, prevIvy.pos, newPos) # Update the growing vector for any collisions growVector = newPos - prevIvy.pos - gravityVector growVector.normalize() # Create a new IvyNode and set its properties tmpNode = IvyNode() tmpNode.climb = climbing tmpNode.pos = newPos tmpNode.primaryDir = prevIvy.primaryDir.lerp(growVector, 0.5) tmpNode.primaryDir.normalize() tmpNode.adhesionVector = adhesionVector tmpNode.length = prevIvy.length + (newPos - prevIvy.pos).length if tmpNode.length > self.maxLength: self.maxLength = tmpNode.length # If the node isn't climbing, update it's floating length # Otherwise set it to 0 if not climbing: tmpNode.floatingLength = prevIvy.floatingLength + (newPos - prevIvy.pos).length else: tmpNode.floatingLength = 0.0 root.ivyNodes.append(tmpNode) # Loop through all roots to check if a new root is generated for root in self.ivyRoots: # Check the root is alive and isn't at high level of recursion if (root.parents > 3) or (not root.alive): continue # Check to make sure there's more than 1 node if len(root.ivyNodes) > 1: # Loop through all nodes in root to check if new root is grown for node in root.ivyNodes: # Set the last node of the root and find the weighting prevIvy = root.ivyNodes[-1] weight = 1.0 - (cos(2.0 * pi * node.length / prevIvy.length) * 0.5 + 0.5) probability = rand_val() # Check if a new root is grown and if so, set its values if (probability * weight > self.branchingProbability): tmpNode = IvyNode() tmpNode.pos = node.pos tmpNode.floatingLength = node.floatingLength tmpRoot = IvyRoot() tmpRoot.parents = root.parents + 1 tmpRoot.ivyNodes.append(tmpNode) self.ivyRoots.append(tmpRoot) return def adhesion(loc, bvhtree, max_l): # Compute the adhesion vector by finding the nearest point nearest_location, *_ = bvhtree.find_nearest(loc, max_l) adhesion_vector = Vector((0.0, 0.0, 0.0)) if nearest_location is not None: # Compute the distance to the nearest point adhesion_vector = nearest_location - loc distance = adhesion_vector.length # If it's less than the maximum allowed and not 0, continue if distance: # Compute the direction vector between the closest point and loc adhesion_vector.normalize() adhesion_vector *= 1.0 - distance / max_l # adhesion_vector *= getFaceWeight(ob.data, nearest_result[3]) return adhesion_vector def collision(bvhtree, pos, new_pos): # Check for collision with the object climbing = False corrected_new_pos = new_pos direction = new_pos - pos hit_location, hit_normal, *_ = bvhtree.ray_cast(pos, direction, direction.length) # If there's a collision we need to check it if hit_location is not None: # Check whether the collision is going into the object if direction.dot(hit_normal) < 0.0: reflected_direction = (new_pos - hit_location).reflect(hit_normal) corrected_new_pos = hit_location + reflected_direction climbing = True return climbing, corrected_new_pos def bvhtree_from_object(ob): import bmesh bm = bmesh.new() depsgraph = bpy.context.evaluated_depsgraph_get() ob_eval = ob.evaluated_get(depsgraph) mesh = ob_eval.to_mesh() bm.from_mesh(mesh) bm.transform(ob.matrix_world) bvhtree = BVHTree.FromBMesh(bm) ob_eval.to_mesh_clear() return bvhtree def check_mesh_faces(ob): me = ob.data if len(me.polygons) > 0: return True return False class IvyGen(Operator): bl_idname = "curve.ivy_gen" bl_label = "IvyGen" bl_description = "Generate Ivy on an Mesh Object" bl_options = {'REGISTER', 'UNDO'} updateIvy: BoolProperty( name="Update Ivy", description="Update the Ivy location based on the cursor and Panel settings", default=False ) defaultIvy: BoolProperty( name="Default Ivy", options={"HIDDEN", "SKIP_SAVE"}, default=False ) @classmethod def poll(self, context): # Check if there's an object and whether it's a mesh ob = context.active_object return ((ob is not None) and (ob.type == 'MESH') and (context.mode == 'OBJECT')) def invoke(self, context, event): self.updateIvy = True return self.execute(context) def execute(self, context): # scene = context.scene ivyProps = context.window_manager.ivy_gen_props if not self.updateIvy: return {'PASS_THROUGH'} # assign the variables, check if it is default # Note: update the values if window_manager props defaults are changed randomSeed = ivyProps.randomSeed if not self.defaultIvy else 0 maxTime = ivyProps.maxTime if not self.defaultIvy else 0 maxIvyLength = ivyProps.maxIvyLength if not self.defaultIvy else 1.0 ivySize = ivyProps.ivySize if not self.defaultIvy else 0.02 maxFloatLength = ivyProps.maxFloatLength if not self.defaultIvy else 0.5 maxAdhesionDistance = ivyProps.maxAdhesionDistance if not self.defaultIvy else 1.0 primaryWeight = ivyProps.primaryWeight if not self.defaultIvy else 0.5 randomWeight = ivyProps.randomWeight if not self.defaultIvy else 0.2 gravityWeight = ivyProps.gravityWeight if not self.defaultIvy else 1.0 adhesionWeight = ivyProps.adhesionWeight if not self.defaultIvy else 0.1 branchingProbability = ivyProps.branchingProbability if not self.defaultIvy else 0.05 leafProbability = ivyProps.leafProbability if not self.defaultIvy else 0.35 ivyBranchSize = ivyProps.ivyBranchSize if not self.defaultIvy else 0.001 ivyLeafSize = ivyProps.ivyLeafSize if not self.defaultIvy else 0.02 growLeaves = ivyProps.growLeaves if not self.defaultIvy else True bpy.ops.object.mode_set(mode='EDIT', toggle=False) bpy.ops.object.mode_set(mode='OBJECT', toggle=False) # Get the selected object ob = context.active_object bvhtree = bvhtree_from_object(ob) # Check if the mesh has at least one polygon since some functions # are expecting them in the object's data (see T51753) check_face = check_mesh_faces(ob) if check_face is False: self.report({'WARNING'}, "Mesh Object doesn't have at least one Face. " "Operation Cancelled") return {"CANCELLED"} # Compute bounding sphere radius # radius = computeBoundingSphere(ob) # Not needed anymore # Get the seeding point seedPoint = context.scene.cursor.location # Fix the random seed rand_seed(randomSeed) # Make the new ivy IVY = Ivy( primaryWeight=primaryWeight, randomWeight=randomWeight, gravityWeight=gravityWeight, adhesionWeight=adhesionWeight, branchingProbability=branchingProbability, leafProbability=leafProbability, ivySize=ivySize, ivyLeafSize=ivyLeafSize, ivyBranchSize=ivyBranchSize, maxFloatLength=maxFloatLength, maxAdhesionDistance=maxAdhesionDistance ) # Generate first root and node IVY.seed(seedPoint) checkTime = False maxLength = maxIvyLength # * radius # If we need to check time set the flag if maxTime != 0.0: checkTime = True t = time.time() startPercent = 0.0 checkAliveIter = [True, ] # Grow until 200 roots is reached or backup counter exceeds limit while (any(checkAliveIter) and (IVY.maxLength < maxLength) and (not checkTime or (time.time() - t < maxTime))): # Grow the ivy for this iteration IVY.grow(ob, bvhtree) # Print the proportion of ivy growth to console if (IVY.maxLength / maxLength * 100) > 10 * startPercent // 10: print('%0.2f%% of Ivy nodes have grown' % (IVY.maxLength / maxLength * 100)) startPercent += 10 if IVY.maxLength / maxLength > 1: print("Halting Growth") # Make an iterator to check if all are alive checkAliveIter = (r.alive for r in IVY.ivyRoots) # Create the curve and leaf geometry createIvyGeometry(IVY, growLeaves) print("Geometry Generation Complete") print("Ivy generated in %0.2f s" % (time.time() - t)) self.updateIvy = False self.defaultIvy = False return {'FINISHED'} def draw(self, context): layout = self.layout layout.prop(self, "updateIvy", icon="FILE_REFRESH") class CURVE_PT_IvyGenPanel(Panel): bl_label = "Ivy Generator" bl_idname = "CURVE_PT_IvyGenPanel" bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Create" bl_context = "objectmode" bl_options = {"DEFAULT_CLOSED"} def draw(self, context): layout = self.layout wm = context.window_manager col = layout.column(align=True) prop_new = col.operator("curve.ivy_gen", text="Add New Ivy", icon="OUTLINER_OB_CURVE") prop_new.defaultIvy = False prop_new.updateIvy = True prop_def = col.operator("curve.ivy_gen", text="Add New Default Ivy", icon="CURVE_DATA") prop_def.defaultIvy = True prop_def.updateIvy = True col = layout.column(align=True) col.label(text="Generation Settings:") col.prop(wm.ivy_gen_props, "randomSeed") col.prop(wm.ivy_gen_props, "maxTime") col = layout.column(align=True) col.label(text="Size Settings:") col.prop(wm.ivy_gen_props, "maxIvyLength") col.prop(wm.ivy_gen_props, "ivySize") col.prop(wm.ivy_gen_props, "maxFloatLength") col.prop(wm.ivy_gen_props, "maxAdhesionDistance") col = layout.column(align=True) col.label(text="Weight Settings:") col.prop(wm.ivy_gen_props, "primaryWeight") col.prop(wm.ivy_gen_props, "randomWeight") col.prop(wm.ivy_gen_props, "gravityWeight") col.prop(wm.ivy_gen_props, "adhesionWeight") col = layout.column(align=True) col.label(text="Branch Settings:") col.prop(wm.ivy_gen_props, "branchingProbability") col.prop(wm.ivy_gen_props, "ivyBranchSize") col = layout.column(align=True) col.prop(wm.ivy_gen_props, "growLeaves") if wm.ivy_gen_props.growLeaves: col = layout.column(align=True) col.label(text="Leaf Settings:") col.prop(wm.ivy_gen_props, "ivyLeafSize") col.prop(wm.ivy_gen_props, "leafProbability") class IvyGenProperties(PropertyGroup): maxIvyLength: FloatProperty( name="Max Ivy Length", description="Maximum ivy length in Blender Units", default=1.0, min=0.0, soft_max=3.0, subtype='DISTANCE', unit='LENGTH' ) primaryWeight: FloatProperty( name="Primary Weight", description="Weighting given to the current direction", default=0.5, min=0.0, soft_max=1.0 ) randomWeight: FloatProperty( name="Random Weight", description="Weighting given to the random direction", default=0.2, min=0.0, soft_max=1.0 ) gravityWeight: FloatProperty( name="Gravity Weight", description="Weighting given to the gravity direction", default=1.0, min=0.0, soft_max=1.0 ) adhesionWeight: FloatProperty( name="Adhesion Weight", description="Weighting given to the adhesion direction", default=0.1, min=0.0, soft_max=1.0 ) branchingProbability: FloatProperty( name="Branching Probability", description="Probability of a new branch forming", default=0.05, min=0.0, soft_max=1.0 ) leafProbability: FloatProperty( name="Leaf Probability", description="Probability of a leaf forming", default=0.35, min=0.0, soft_max=1.0 ) ivySize: FloatProperty( name="Ivy Size", description="The length of an ivy segment in Blender" " Units", default=0.02, min=0.0, soft_max=1.0, precision=3 ) ivyLeafSize: FloatProperty( name="Ivy Leaf Size", description="The size of the ivy leaves", default=0.02, min=0.0, soft_max=0.5, precision=3 ) ivyBranchSize: FloatProperty( name="Ivy Branch Size", description="The size of the ivy branches", default=0.001, min=0.0, soft_max=0.1, precision=4 ) maxFloatLength: FloatProperty( name="Max Float Length", description="The maximum distance that a branch " "can live while floating", default=0.5, min=0.0, soft_max=1.0 ) maxAdhesionDistance: FloatProperty( name="Max Adhesion Length", description="The maximum distance that a branch " "will feel the effects of adhesion", default=1.0, min=0.0, soft_max=2.0, precision=2 ) randomSeed: IntProperty( name="Random Seed", description="The seed governing random generation", default=0, min=0 ) maxTime: FloatProperty( name="Maximum Time", description="The maximum time to run the generation for " "in seconds generation (0.0 = Disabled)", default=0.0, min=0.0, soft_max=10 ) growLeaves: BoolProperty( name="Grow Leaves", description="Grow leaves or not", default=True ) classes = ( IvyGen, IvyGenProperties, CURVE_PT_IvyGenPanel ) def register(): for cls in classes: bpy.utils.register_class(cls) bpy.types.WindowManager.ivy_gen_props = PointerProperty( type=IvyGenProperties ) def unregister(): del bpy.types.WindowManager.ivy_gen_props for cls in reversed(classes): bpy.utils.unregister_class(cls) if __name__ == "__main__": register()