diff --git a/add_curve_sapling/__init__.py b/add_curve_sapling/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..56c51bbfa52d093b01d729435b63936c09d007de --- /dev/null +++ b/add_curve_sapling/__init__.py @@ -0,0 +1,587 @@ +#====================== BEGIN GPL LICENSE BLOCK ====================== +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#======================= END GPL LICENSE BLOCK ======================== + +bl_info = { + "name": "Sapling", + "author": "Andrew Hale (TrumanBlending)", + "version": (0, 2, 2), + "blender": (2, 5, 8), + "api": 37702, + "location": "View3D > Add > Curve", + "description": ("Adds a parametric tree. The method is presented by " + "Jason Weber & Joseph Penn in their paper 'Creation and Rendering of " + "Realistic Trees'."), + "warning": "", # used for warning icon and text in addons panel + "wiki_url": "", + "tracker_url": "", + "category": "Add Curve"} + +if "bpy" in locals(): + import imp + imp.reload(utils) +else: + from add_curve_sapling import utils + +import bpy +import time +import os + +#from utils import * +from mathutils import * +from math import pi, sin, degrees, radians, atan2, copysign +from random import random, uniform, seed, choice, getstate, setstate +from bpy.props import * + +from add_curve_sapling.utils import * + +#global splitError +useSet = False + +shapeList = [('0', 'Conical (0)', 'Shape = 0'), + ('1', 'Spherical (1)', 'Shape = 1'), + ('2', 'Hemispherical (2)', 'Shape = 2'), + ('3', 'Cylindrical (3)', 'Shape = 3'), + ('4', 'Tapered Cylindrical (4)', 'Shape = 4'), + ('5', 'Flame (5)', 'Shape = 5'), + ('6', 'Inverse Conical (6)', 'Shape = 6'), + ('7', 'Tend Flame (7)', 'Shape = 7')] + +handleList = [('0', 'Auto', 'Auto'), + ('1', 'Vector', 'Vector')] + +settings = [('0', 'Geometry', 'Geometry'), + ('1', 'Branch Splitting', 'Branch Splitting'), + ('2', 'Branch Growth', 'Branch Growth'), + ('3', 'Pruning', 'Pruning'), + ('4', 'Leaves', 'Leaves'), + ('5', 'Armature', 'Armature')] + + +def getPresetpath(): + '''Support user defined scripts directory + Find the first ocurrence of add_curve_sapling/presets in possible script paths + and return it as preset path''' + presetpath = "" + for p in bpy.utils.script_paths(): + presetpath = os.path.join(p, 'addons', 'add_curve_sapling', 'presets') + if os.path.exists(presetpath): + break + return presetpath + + +class ExportData(bpy.types.Operator): + '''This operator handles writing presets to file''' + bl_idname = 'sapling.exportdata' + bl_label = 'Export Preset' + + data = StringProperty() + + def execute(self, context): + # Unpack some data from the input + data, filename = eval(self.data) + try: + # Check whether the file exists by trying to open it. + f = open(os.path.join(getPresetpath(), filename + '.py'), 'r') + f.close() + # If it exists then report an error + self.report({'ERROR_INVALID_INPUT'}, 'Preset Already Exists') + return {'CANCELLED'} + except IOError: + if data: + # If it doesn't exist, create the file with the required data + f = open(os.path.join(getPresetpath(), filename + '.py'), 'w') + f.write(data) + f.close() + return {'FINISHED'} + else: + return {'CANCELLED'} + + +class ImportData(bpy.types.Operator): + '''This operator handles importing existing presets''' + bl_idname = 'sapling.importdata' + bl_label = 'Import Preset' + + filename = StringProperty() + + def execute(self, context): + # Make sure the operator knows about the global variables + global settings, useSet + # Read the preset data into the global settings + f = open(os.path.join(getPresetpath(), self.filename), 'r') + settings = f.readline() + f.close() + #print(settings) + settings = eval(settings) + # Set the flag to use the settings + useSet = True + return {'FINISHED'} + + +class PresetMenu(bpy.types.Menu): + '''Create the preset menu by finding all preset files + in the preset directory + ''' + bl_idname = "sapling.presetmenu" + bl_label = "Presets" + + def draw(self, context): + # Get all the sapling presets + presets = [a for a in os.listdir(getPresetpath()) if a[-3:] == '.py'] + layout = self.layout + # Append all to the menu + for p in presets: + layout.operator("sapling.importdata", text=p[:-3]).filename = p + + +class AddTree(bpy.types.Operator): + bl_idname = "curve.tree_add" + bl_label = "Sapling" + bl_options = {'REGISTER', 'UNDO'} + + chooseSet = EnumProperty(name='Settings', + description='Choose the settings to modify', + items=settings, + default='0') + bevel = BoolProperty(name='Bevel', + description='Whether the curve is bevelled', + default=False) + prune = BoolProperty(name='Prune', + description='Whether the tree is pruned', + default=False) + showLeaves = BoolProperty(name='Show Leaves', + description='Whether the leaves are shown', + default=False) + useArm = BoolProperty(name='Use Armature', + description='Whether the armature is generated', + default=False) + seed = IntProperty(name='Random Seed', + description='The seed of the random number generator', + default=0) + handleType = IntProperty(name='Handle Type', + description='The type of curve handles', + min=0, + max=1, + default=0) + levels = IntProperty(name='Levels', + description='Number of recursive branches (Levels)', + min=1, + max=6, + default=3) + length = FloatVectorProperty(name='Length', + description='The relative lengths of each branch level (nLength)', + min=0.0, + default=[1, 0.3, 0.6, 0.45], + size=4) + lengthV = FloatVectorProperty(name='Length Variation', + description='The relative length variations of each level (nLengthV)', + min=0.0, + default=[0, 0, 0, 0], + size=4) + branches = IntVectorProperty(name='Branches', + description='The number of branches grown at each level (nBranches)', + min=0, + default=[50, 30, 10, 10], + size=4) + curveRes = IntVectorProperty(name='Curve Resolution', + description='The number of segments on each branch (nCurveRes)', + min=1, + default=[3, 5, 3, 1], + size=4) + curve = FloatVectorProperty(name='Curvature', + description='The angle of the end of the branch (nCurve)', + default=[0, -40, -40, 0], + size=4) + curveV = FloatVectorProperty(name='Curvature Variation', + description='Variation of the curvature (nCurveV)', + default=[20, 50, 75, 0], + size=4) + curveBack = FloatVectorProperty(name='Back Curvature', + description='Curvature for the second half of a branch (nCurveBack)', + default=[0, 0, 0, 0], + size=4) + baseSplits = IntProperty(name='Base Splits', + description='Number of trunk splits at its base (nBaseSplits)', + min=0, + default=0) + segSplits = FloatVectorProperty(name='Segment Splits', + description='Number of splits per segment (nSegSplits)', + min=0, + default=[0, 0, 0, 0], + size=4) + splitAngle = FloatVectorProperty(name='Split Angle', + description='Angle of branch splitting (nSplitAngle)', + default=[0, 0, 0, 0], + size=4) + splitAngleV = FloatVectorProperty(name='Split Angle Variation', + description='Variation in the split angle (nSplitAngleV)', + default=[0, 0, 0, 0], + size=4) + scale = FloatProperty(name='Scale', + description='The tree scale (Scale)', + min=0.0, + default=13.0) + scaleV = FloatProperty(name='Scale Variation', + description='The variation in the tree scale (ScaleV)', + default=3.0) + attractUp = FloatProperty(name='Vertical Attraction', + description='Branch upward attraction', + default=0.0) + shape = EnumProperty(name='Shape', + description='The overall shape of the tree (Shape)', + items=shapeList, + default='7') + baseSize = FloatProperty(name='Base Size', + description='Fraction of tree height with no branches (BaseSize)', + min=0.0, + max=1.0, + default=0.4) + ratio = FloatProperty(name='Ratio', + description='Base radius size (Ratio)', + min=0.0, + default=0.015) + taper = FloatVectorProperty(name='Taper', + description='The fraction of tapering on each branch (nTaper)', + min=0.0, + max=1.0, + default=[1, 1, 1, 1], + size=4) + ratioPower = FloatProperty(name='Branch Radius Ratio', + description=('Power which defines the radius of a branch compared to ' + 'the radius of the branch it grew from (RatioPower)'), + min=0.0, + default=1.2) + downAngle = FloatVectorProperty(name='Down Angle', + description=('The angle between a new branch and the one it grew ' + 'from (nDownAngle)'), + default=[90, 60, 45, 45], + size=4) + downAngleV = FloatVectorProperty(name='Down Angle Variation', + description='Variation in the down angle (nDownAngleV)', + default=[0, -50, 10, 10], + size=4) + rotate = FloatVectorProperty(name='Rotate Angle', + description=('The angle of a new branch around the one it grew from ' + '(nRotate)'), + default=[140, 140, 140, 77], + size=4) + rotateV = FloatVectorProperty(name='Rotate Angle Variation', + description='Variation in the rotate angle (nRotateV)', + default=[0, 0, 0, 0], + size=4) + scale0 = FloatProperty(name='Radius Scale', + description='The scale of the trunk radius (0Scale)', + min=0.0, + default=1.0) + scaleV0 = FloatProperty(name='Radius Scale Variation', + description='Variation in the radius scale (0ScaleV)', + default=0.2) + pruneWidth = FloatProperty(name='Prune Width', + description='The width of the envelope (PruneWidth)', + min=0.0, + default=0.4) + pruneWidthPeak = FloatProperty(name='Prune Width Peak', + description=('Fraction of envelope height where the maximum width ' + 'occurs (PruneWidthPeak)'), + min=0.0, + default=0.6) + prunePowerHigh = FloatProperty(name='Prune Power High', + description=('Power which determines the shape of the upper portion ' + 'of the envelope (PrunePowerHigh)'), + default=0.5) + prunePowerLow = FloatProperty(name='Prune Power Low', + description=('Power which determines the shape of the lower portion ' + 'of the envelope (PrunePowerLow)'), + default=0.001) + pruneRatio = FloatProperty(name='Prune Ratio', + description='Proportion of pruned length (PruneRatio)', + min=0.0, + max=1.0, + default=1.0) + leaves = IntProperty(name='Leaves', + description='Maximum number of leaves per branch (Leaves)', + default=25) + leafScale = FloatProperty(name='Leaf Scale', + description='The scaling applied to the whole leaf (LeafScale)', + min=0.0, + default=0.17) + leafScaleX = FloatProperty(name='Leaf Scale X', + description=('The scaling applied to the x direction of the leaf ' + '(LeafScaleX)'), + min=0.0, + default=1.0) + bend = FloatProperty(name='Leaf Bend', + description='The proportion of bending applied to the leaf (Bend)', + min=0.0, + max=1.0, + default=0.0) + leafDist = EnumProperty(name='Leaf Distribution', + description='The way leaves are distributed on branches', + items=shapeList, + default='4') + bevelRes = IntProperty(name='Bevel Resolution', + description='The bevel resolution of the curves', + min=0, + default=0) + resU = IntProperty(name='Curve Resolution', + description='The resolution along the curves', + min=1, + default=4) + handleType = EnumProperty(name='Handle Type', + description='The type of handles used in the spline', + items=handleList, + default='1') + frameRate = FloatProperty(name='Frame Rate', + description=('The number of frames per second which can be used to ' + 'adjust the speed of animation'), + min=0.001, + default=1) + windSpeed = FloatProperty(name='Wind Speed', + description='The wind speed to apply to the armature (WindSpeed)', + default=2.0) + windGust = FloatProperty(name='Wind Gust', + description='The greatest increase over Wind Speed (WindGust)', + default=0.0) + armAnim = BoolProperty(name='Armature Animation', + description='Whether animation is added to the armature', + default=False) + + presetName = StringProperty(name='Preset Name', + description='The name of the preset to be saved', + default='', + subtype='FILENAME') + limitImport = BoolProperty(name='Limit Import', + description='Limited imported tree to 2 levels & no leaves for speed', + default=True) + + startCurv = FloatProperty(name='Trunk Starting Angle', + description=('The angle between vertical and the starting direction ' + 'of the trunk'), + min=0.0, + max=360, + default=0.0) + + @classmethod + def poll(cls, context): + return context.mode == 'OBJECT' + + def draw(self, context): + + layout = self.layout + + # Branch specs + #layout.label('Tree Definition') + +# row = layout.row() +# row.prop(self, 'chooseSet') + +# if self.chooseSet == '0': + box = layout.box() + box.label('Geometry') + row = box.row() + row.prop(self, 'bevel') + row = box.row() + row.prop(self, 'bevelRes') + row.prop(self, 'resU') + row = box.row() + row.prop(self, 'handleType') + row = box.row() + row.prop(self, 'shape') + row = box.row() + row.prop(self, 'seed') + row = box.row() + row.prop(self, 'ratio') + row = box.row() + col = row.column() + col.prop(self, 'scale') + col = row.column() + col.prop(self, 'scaleV') + row = box.row() + col = row.column() + col.prop(self, 'scale0') + col = row.column() + col.prop(self, 'scaleV0') + + # Here we create a dict of all the properties. + # Unfortunately as_keyword doesn't work with vector properties, + # so we need something custom. This is it + data = [] + for a, b in (self.as_keywords(ignore=("presetName", "limitImport"))).items(): + # If the property is a vector property then evaluate it and + # convert to a string + if (repr(b))[:3] == 'bpy': + data.append((a, eval('(self.' + a + ')[:]'))) + # Otherwise, it is fine so just add it + else: + data.append((a, b)) + # Create the dict from the list + data = dict(data) + + row = box.row() + row.prop(self, 'presetName') + # Send the data dict and the file name to the exporter + row.operator('sapling.exportdata').data = repr([repr(data), + self.presetName]) + row = box.row() + row.menu('sapling.presetmenu', text='Load Preset') + row.prop(self, 'limitImport') + +# if self.chooseSet == '1': + box = layout.box() + box.label('Branch Splitting') + row = box.row() + row.prop(self, 'levels') + row = box.row() + row.prop(self, 'baseSplits') + row = box.row() + row.prop(self, 'baseSize') + row = box.row() + col = row.column() + col.prop(self, 'branches') + col = row.column() + col.prop(self, 'segSplits') + row = box.row() + col = row.column() + col.prop(self, 'splitAngle') + col = row.column() + col.prop(self, 'splitAngleV') + row = box.row() + col = row.column() + col.prop(self, 'downAngle') + col = row.column() + col.prop(self, 'downAngleV') + row = box.row() + col = row.column() + col.prop(self, 'rotate') + col = row.column() + col.prop(self, 'rotateV') + row = box.row() + col = row.column() + col.prop(self, 'ratioPower') + +# if self.chooseSet == '2': + box = layout.box() + box.label('Branch Growth') + row = box.row() + row.prop(self, 'startCurv') + row = box.row() + row.prop(self, 'attractUp') + row = box.row() + col = row.column() + col.prop(self, 'length') + col = row.column() + col.prop(self, 'lengthV') + row = box.row() + col = row.column() + col.prop(self, 'curve') + col = row.column() + col.prop(self, 'curveV') + row = box.row() + col = row.column() + col.prop(self, 'curveBack') + col = row.column() + col.prop(self, 'taper') + row = box.row() + col = row.column() + col.prop(self, 'curveRes') + +# if self.chooseSet == '3': + box = layout.box() + box.label('Pruning') + row = box.row() + row.prop(self, 'prune') + row = box.row() + row.prop(self, 'pruneRatio') + row = box.row() + row.prop(self, 'pruneWidth') + row = box.row() + row.prop(self, 'pruneWidthPeak') + row = box.row() + row.prop(self, 'prunePowerHigh') + row.prop(self, 'prunePowerLow') + +# if self.chooseSet == '4': + box = layout.box() + box.label('Leaves') + row = box.row() + row.prop(self, 'showLeaves') + row = box.row() + row.prop(self, 'leaves') + row = box.row() + row.prop(self, 'leafDist') + row = box.row() + col = row.column() + col.prop(self, 'leafScale') + col = row.column() + col.prop(self, 'leafScaleX') + row = box.row() + row.prop(self, 'bend') + +# if self.chooseSet == '5': + box = layout.box() + box.label('Armature and Animation') + row = box.row() + row.prop(self, 'useArm') + row.prop(self, 'armAnim') + row = box.row() + row.prop(self, 'windSpeed') + row.prop(self, 'windGust') + row = box.row() + row.prop(self, 'frameRate') + + def execute(self, context): + # Ensure the use of the global variables + global settings, useSet + start_time = time.time() + #bpy.ops.ImportData.filename = "quaking_aspen" + # If we need to set the properties from a preset then do it here + if useSet: + for a, b in settings.items(): + setattr(self, a, b) + if self.limitImport: + setattr(self, 'levels', 2) + setattr(self, 'showLeaves', False) + useSet = False + addTree(self) + print("Tree creation in %0.1fs" %(time.time()-start_time)) + return {'FINISHED'} + + def invoke(self, context, event): +# global settings, useSet +# useSet = True + bpy.ops.sapling.importdata(filename = "quaking_aspen.py") + return self.execute(context) + + +def menu_func(self, context): + self.layout.operator(AddTree.bl_idname, text="Add Tree", icon='PLUGIN') + + +def register(): + bpy.utils.register_module(__name__) + + bpy.types.INFO_MT_curve_add.append(menu_func) + + +def unregister(): + bpy.utils.unregister_module(__name__) + + bpy.types.INFO_MT_curve_add.remove(menu_func) + +if __name__ == "__main__": + register() \ No newline at end of file diff --git a/add_curve_sapling/presets/black_tupelo.py b/add_curve_sapling/presets/black_tupelo.py new file mode 100644 index 0000000000000000000000000000000000000000..7eee5178d0f186366164bcb724eb3c831bbc0ced --- /dev/null +++ b/add_curve_sapling/presets/black_tupelo.py @@ -0,0 +1 @@ +{'pruneWidthPeak': 0.6000000238418579, 'downAngleV': (0.0, -40.0, 10.0, 10.0), 'frameRate': 1.0, 'lengthV': (0.0, 0.05000000074505806, 0.10000000149011612, 0.0), 'shape': '4', 'seed': 0, 'bend': 0.0, 'armAnim': False, 'useArm': False, 'splitAngle': (0.0, 0.0, 0.0, 0.0), 'baseSize': 0.20000000298023224, 'baseSplits': 0, 'scaleV': 5.0, 'scale': 23.0, 'ratio': 0.014999999664723873, 'curveV': (40.0, 90.0, 150.0, 0.0), 'prunePowerHigh': 0.5, 'splitAngleV': (0.0, 0.0, 0.0, 0.0), 'resU': 4, 'segSplits': (0.0, 0.0, 0.0, 0.0), 'ratioPower': 1.2999999523162842, 'handleType': '1', 'length': (1.0, 0.30000001192092896, 0.6000000238418579, 0.4000000059604645), 'rotateV': (0.0, 0.0, 0.0, 0.0), 'attractUp': 0.5, 'scale0': 1.0, 'bevel': False, 'leafDist': '4', 'chooseSet': '0', 'levels': 4, 'downAngle': (90.0, 60.0, 30.0, 45.0), 'showLeaves': False, 'prunePowerLow': 0.0010000000474974513, 'scaleV0': 0.0, 'leafScaleX': 0.5, 'curveRes': (10, 10, 10, 1), 'rotate': (140.0, 140.0, 140.0, 140.0), 'branches': (0, 50, 25, 12), 'prune': False, 'bevelRes': 0, 'taper': (1.0, 1.0, 1.0, 1.0), 'pruneRatio': 1.0, 'leaves': 6, 'curve': (0.0, 0.0, -10.0, 0.0), 'leafScale': 0.30000001192092896, 'windSpeed': 2.0, 'pruneWidth': 0.4000000059604645, 'windGust': 0.0, 'startCurv': 0.0, 'curveBack': (0.0, 0.0, 0.0, 0.0)} \ No newline at end of file diff --git a/add_curve_sapling/presets/ca_black_oak.py b/add_curve_sapling/presets/ca_black_oak.py new file mode 100644 index 0000000000000000000000000000000000000000..cbe7fe5655a247313353c2648c166bf3bc7a6cf8 --- /dev/null +++ b/add_curve_sapling/presets/ca_black_oak.py @@ -0,0 +1 @@ +{'pruneWidthPeak': 0.6000000238418579, 'downAngleV': (0.0, -30.0, 10.0, 10.0), 'frameRate': 1.0, 'lengthV': (0.0, 0.10000000149011612, 0.05000000074505806, 0.0), 'shape': '2', 'seed': 0, 'bend': 0.0, 'armAnim': False, 'useArm': False, 'splitAngle': (10.0, 10.0, 10.0, 0.0), 'baseSize': 0.05000000074505806, 'baseSplits': 2, 'scaleV': 10.0, 'scale': 10.0, 'ratio': 0.017999999225139618, 'curveV': (90.0, 150.0, -30.0, 0.0), 'prunePowerHigh': 0.5, 'splitAngleV': (0.0, 10.0, 10.0, 0.0), 'resU': 4, 'segSplits': (0.4000000059604645, 0.20000000298023224, 0.10000000149011612, 0.0), 'ratioPower': 1.2999999523162842, 'handleType': '1', 'length': (1.0, 0.800000011920929, 0.20000000298023224, 0.4000000059604645), 'rotateV': (0.0, 0.0, 0.0, 0.0), 'attractUp': 0.800000011920929, 'scale0': 1.0, 'bevel': False, 'leafDist': '4', 'chooseSet': '0', 'levels': 3, 'downAngle': (0.0, 30.0, 45.0, 45.0), 'showLeaves': False, 'prunePowerLow': 0.0010000000474974513, 'scaleV0': 0.0, 'leafScaleX': 0.6600000262260437, 'curveRes': (8, 10, 3, 1), 'rotate': (0.0, 80.0, 140.0, 140.0), 'branches': (0, 40, 120, 0), 'prune': False, 'bevelRes': 0, 'taper': (1.0, 1.0, 1.0, 1.0), 'pruneRatio': 1.0, 'leaves': 25, 'curve': (0.0, 40.0, 0.0, 0.0), 'leafScale': 0.11999999731779099, 'windSpeed': 2.0, 'pruneWidth': 0.4000000059604645, 'windGust': 0.0, 'startCurv': 0.0, 'curveBack': (0.0, -70.0, 0.0, 0.0)} \ No newline at end of file diff --git a/add_curve_sapling/presets/quaking_aspen.py b/add_curve_sapling/presets/quaking_aspen.py new file mode 100644 index 0000000000000000000000000000000000000000..39e1a762206ace491740c168ff8a93dc0355bebc --- /dev/null +++ b/add_curve_sapling/presets/quaking_aspen.py @@ -0,0 +1 @@ +{'pruneWidthPeak': 0.6000000238418579, 'downAngleV': (0.0, -50.0, 10.0, 10.0), 'frameRate': 1.0, 'lengthV': (0.0, 0.0, 0.0, 0.0), 'shape': '7', 'seed': 0, 'bend': 0.0, 'armAnim': False, 'useArm': False, 'splitAngle': (0.0, 0.0, 0.0, 0.0), 'baseSize': 0.4000000059604645, 'baseSplits': 0, 'scaleV': 3.0, 'scale': 13.0, 'ratio': 0.014999999664723873, 'curveV': (20.0, 50.0, 75.0, 0.0), 'prunePowerHigh': 0.5, 'splitAngleV': (0.0, 0.0, 0.0, 0.0), 'resU': 4, 'segSplits': (0.0, 0.0, 0.0, 0.0), 'ratioPower': 1.2000000476837158, 'handleType': '1', 'length': (1.0, 0.30000001192092896, 0.6000000238418579, 0.44999998807907104), 'rotateV': (0.0, 0.0, 0.0, 0.0), 'attractUp': 0.5, 'scale0': 1.0, 'bevel': False, 'leafDist': '4', 'chooseSet': '0', 'levels': 3, 'downAngle': (90.0, 60.0, 45.0, 45.0), 'showLeaves': False, 'prunePowerLow': 0.0010000000474974513, 'scaleV0': 0.20000000298023224, 'leafScaleX': 1.0, 'curveRes': (3, 5, 3, 1), 'rotate': (140.0, 140.0, 140.0, 77.0), 'branches': (0, 50, 30, 10), 'prune': False, 'bevelRes': 0, 'taper': (1.0, 1.0, 1.0, 1.0), 'pruneRatio': 1.0, 'leaves': 25, 'curve': (0.0, -40.0, -40.0, 0.0), 'leafScale': 0.17000000178813934, 'windSpeed': 2.0, 'pruneWidth': 0.4000000059604645, 'windGust': 0.0, 'startCurv': 0.0, 'curveBack': (0.0, 0.0, 0.0, 0.0)} \ No newline at end of file diff --git a/add_curve_sapling/presets/weeping_willow.py b/add_curve_sapling/presets/weeping_willow.py new file mode 100644 index 0000000000000000000000000000000000000000..7dedec9c5e88469917e40838c27798d305b0f813 --- /dev/null +++ b/add_curve_sapling/presets/weeping_willow.py @@ -0,0 +1 @@ +{'pruneWidthPeak': 0.6000000238418579, 'downAngleV': (0.0, 10.0, 10.0, 10.0), 'frameRate': 1.0, 'lengthV': (0.0, 0.10000000149011612, 0.0, 0.0), 'shape': '3', 'seed': 0, 'bend': 0.0, 'armAnim': False, 'useArm': False, 'splitAngle': (3.0, 30.0, 45.0, 0.0), 'baseSize': 0.05000000074505806, 'baseSplits': 2, 'scaleV': 5.0, 'scale': 15.0, 'ratio': 0.029999999329447746, 'curveV': (120.0, 90.0, 0.0, 0.0), 'prunePowerHigh': 0.5, 'splitAngleV': (0.0, 10.0, 20.0, 0.0), 'resU': 4, 'segSplits': (0.10000000149011612, 0.20000000298023224, 0.20000000298023224, 0.0), 'ratioPower': 2.0, 'handleType': '1', 'length': (0.800000011920929, 0.5, 1.5, 0.10000000149011612), 'rotateV': (0.0, 30.0, 30.0, 0.0), 'attractUp': -3.0, 'scale0': 1.0, 'bevel': False, 'leafDist': '4', 'chooseSet': '0', 'levels': 4, 'downAngle': (0.0, 20.0, 30.0, 20.0), 'showLeaves': False, 'prunePowerLow': 0.0010000000474974513, 'scaleV0': 0.0, 'leafScaleX': 0.20000000298023224, 'curveRes': (8, 16, 12, 1), 'rotate': (0.0, -120.0, -120.0, 140.0), 'branches': (0, 25, 10, 300), 'prune': True, 'bevelRes': 0, 'taper': (1.0, 1.0, 1.0, 1.0), 'pruneRatio': 1.0, 'leaves': 15, 'curve': (0.0, 40.0, 0.0, 0.0), 'leafScale': 0.11999999731779099, 'windSpeed': 2.0, 'pruneWidth': 0.4000000059604645, 'windGust': 0.0, 'startCurv': 0.0, 'curveBack': (20.0, 80.0, 0.0, 0.0)} \ No newline at end of file diff --git a/add_curve_sapling/utils.py b/add_curve_sapling/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..bb9a6298e7898f8b5f6d4fcd32238922dedcc399 --- /dev/null +++ b/add_curve_sapling/utils.py @@ -0,0 +1,907 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + + +import bpy +import time +import copy + +from mathutils import * +from math import pi,sin,degrees,radians,atan2,copysign,cos,acos +from random import random,uniform,seed,choice,getstate,setstate +from bpy.props import * +from collections import deque + +# Initialise the split error and axis vectors +splitError = 0.0 +zAxis = Vector((0,0,1)) +yAxis = Vector((0,1,0)) +xAxis = Vector((1,0,0)) + +# This class will contain a part of the tree which needs to be extended and the required tree parameters +class stemSpline: + def __init__(self,spline,curvature,curvatureV,segments,maxSegs,segLength,childStems,stemRadStart,stemRadEnd,splineNum): + self.spline = spline + self.p = spline.bezier_points[-1] + self.curv = curvature + self.curvV = curvatureV + self.seg = segments + self.segMax = maxSegs + self.segL = segLength + self.children = childStems + self.radS = stemRadStart + self.radE = stemRadEnd + self.splN = splineNum + # This method determines the quaternion of the end of the spline + def quat(self): + if len(self.spline.bezier_points) == 1: + return ((self.spline.bezier_points[-1].handle_right - self.spline.bezier_points[-1].co).normalized()).to_track_quat('Z','Y') + else: + return ((self.spline.bezier_points[-1].co - self.spline.bezier_points[-2].co).normalized()).to_track_quat('Z','Y') + # Determine the declination + def dec(self): + tempVec = zAxis.copy() + tempVec.rotate(self.quat()) + return zAxis.angle(tempVec) + # Update the end of the spline and increment the segment count + def updateEnd(self): + self.p = self.spline.bezier_points[-1] + self.seg += 1 + # Determine the spread angle for a split + def spreadAng(self): + return radians(choice([-1,1])*(20 + 0.75*(30 + abs(degrees(self.dec()) - 90))*random()**2)) + # Determine the splitting angle for a split + def splitAngle(self,splitAng,splitAngV): + return max(0,splitAng+uniform(-splitAngV,splitAngV)-self.dec()) + # This is used to change the the curvature per segment of the spline + def curvAdd(self,curvD): + self.curv += curvD + +# This class contains the data for a point where a new branch will sprout +class childPoint: + def __init__(self,coords,quat,radiusPar,offset,lengthPar,parBone): + self.co = coords + self.quat = quat + self.radiusPar = radiusPar + self.offset = offset + self.lengthPar = lengthPar + self.parBone = parBone + + +# This function calculates the shape ratio as defined in the paper +def shapeRatio(shape,ratio,pruneWidthPeak=0.0,prunePowerHigh=0.0,prunePowerLow=0.0): + if shape == 0: + return 0.2 + 0.8*ratio + elif shape == 1: + return 0.2 + 0.8*sin(pi*ratio) + elif shape == 2: + return 0.2 + 0.8*sin(0.5*pi*ratio) + elif shape == 3: + return 1.0 + elif shape == 4: + return 0.5 + 0.5*ratio + elif shape == 5: + if ratio <= 0.7: + return ratio/0.7 + else: + return (1.0 - ratio)/0.3 + elif shape == 6: + return 1.0 - 0.8*ratio + elif shape == 7: + if ratio <= 0.7: + return 0.5 + 0.5*ratio/0.7 + else: + return 0.5 + 0.5*(1.0 - ratio)/0.3 + elif shape == 8: + if (ratio < (1 - pruneWidthPeak)) and (ratio > 0.0): + return ((ratio/(1 - pruneWidthPeak))**prunePowerHigh) + elif (ratio >= (1 - pruneWidthPeak)) and (ratio < 1.0): + return (((1 - ratio)/pruneWidthPeak)**prunePowerLow) + else: + return 0.0 + +# This function determines the actual number of splits at a given point using the global error +def splits(n): + global splitError + nEff = round(n + splitError,0) + splitError -= (nEff - n) + return int(nEff) + +# Determine the declination from a given quaternion +def declination(quat): + tempVec = zAxis.copy() + tempVec.rotate(quat) + tempVec.normalize() + return degrees(acos(tempVec.z)) + +# Determine the length of a child stem +def lengthChild(lMax,offset,lPar,shape=False,lBase=None): + if shape: + return lPar*lMax*shapeRatio(shape,(lPar - offset)/(lPar - lBase)) + else: + return lMax*(lPar - 0.6*offset) + +# Find the actual downAngle taking into account the special case +def downAngle(downAng,downAngV,lPar=None,offset=None,lBase=None): + if downAngV < 0: + return downAng + (uniform(-downAngV,downAngV)*(1 - 2*shapeRatio(0,(lPar - offset)/(lPar - lBase)))) + else: + return downAng + uniform(-downAngV,downAngV) + +# Returns the rotation matrix equivalent to i rotations by 2*pi/(n+1) +def splitRotMat(n,i): + return (Matrix()).Rotation(2*i*pi/(n+1),3,'Z') + +# Returns the split angle +def angleSplit(splitAng,splitAngV,quat): + return max(0,splitAng+uniform(-splitAngV,splitAngV)-declination(quat)) + +# Returns number of stems a stem will sprout +def stems(stemsMax,lPar,offset,lChild=False,lChildMax=None): + if lChild: + return stemsMax*(0.2 + 0.8*(lChild/lPar)/lChildMax) + else: + return stemsMax*(1.0 - 0.5*offset/lPar) + +# Returns the spreading angle +def spreadAng(dec): + return radians(choice([-1,1])*(20 + 0.75*(30 + abs(dec - 90))*random()**2)) + +# Determines the angle of upward rotation of a segment due to attractUp +def curveUp(attractUp,quat,curveRes): + tempVec = yAxis.copy() + tempVec.rotate(quat) + tempVec.normalize() + return attractUp*radians(declination(quat))*abs(tempVec.z)/curveRes + +# Evaluate a bezier curve for the parameter 0<=t<=1 along its length +def evalBez(p1,h1,h2,p2,t): + return ((1-t)**3)*p1 + (3*t*(1-t)**2)*h1 + (3*(t**2)*(1-t))*h2 + (t**3)*p2 + +# Evaluate the unit tangent on a bezier curve for t +def evalBezTan(p1,h1,h2,p2,t): + return ((-3*(1-t)**2)*p1 + (-6*t*(1-t) + 3*(1-t)**2)*h1 + (-3*(t**2) + 6*t*(1-t))*h2 + (3*t**2)*p2).normalized() + +# Determine the range of t values along a splines length where child stems are formed +def findChildPoints(stemList,numChild): + numPoints = sum([len(n.spline.bezier_points) for n in stemList]) + numSplines = len(stemList) + numSegs = numPoints - numSplines + numPerSeg = numChild/numSegs + numMain = round(numPerSeg*stemList[0].segMax,0) + return [(a+1)/(numMain) for a in range(int(numMain))] + +# Find the coordinates, quaternion and radius for each t on the stem +def interpStem(stem,tVals,lPar,parRad): + tempList = deque() + addpoint = tempList.append + checkVal = (stem.segMax - len(stem.spline.bezier_points) + 1)/stem.segMax + points = stem.spline.bezier_points + numPoints = len(stem.spline.bezier_points) + # Loop through all the parametric values to be determined + for t in tVals: + if (t >= checkVal) and (t < 1.0): + scaledT = (t-checkVal)/(tVals[-1]-checkVal) + length = (numPoints-1)*t#scaledT + index = int(length) + if scaledT == 1.0: + coord = points[-1].co + quat = (points[-1].handle_right - points[-1].co).to_track_quat('Z','Y') + radius = parRad#points[-2].radius + else: + tTemp = length - index + coord = evalBez(points[index].co,points[index].handle_right,points[index+1].handle_left,points[index+1].co,tTemp) + quat = (evalBezTan(points[index].co,points[index].handle_right,points[index+1].handle_left,points[index+1].co,tTemp)).to_track_quat('Z','Y') + radius = (1-tTemp)*points[index].radius + tTemp*points[index+1].radius # Not sure if this is the parent radius at the child point or parent start radius + addpoint(childPoint(coord,quat,(parRad, radius),t*lPar,lPar,'bone'+(str(stem.splN).rjust(3,'0'))+'.'+(str(index).rjust(3,'0')))) + return tempList + +# Convert a list of degrees to radians +def toRad(list): + return [radians(a) for a in list] + +# This is the function which extends (or grows) a given stem. +def growSpline(stem,numSplit,splitAng,splitAngV,splineList,attractUp,hType,splineToBone): + # First find the current direction of the stem + dir = stem.quat() + # If the stem splits, we need to add new splines etc + if numSplit > 0: + # Get the curve data + cuData = stem.spline.id_data.name + cu = bpy.data.curves[cuData] + # Now for each split add the new spline and adjust the growth direction + for i in range(numSplit): + newSpline = cu.splines.new('BEZIER') + newPoint = newSpline.bezier_points[-1] + (newPoint.co,newPoint.handle_left_type,newPoint.handle_right_type) = (stem.p.co,'VECTOR','VECTOR') + newPoint.radius = stem.radS*(1 - stem.seg/stem.segMax) + stem.radE*(stem.seg/stem.segMax) + + # Here we make the new "sprouting" stems diverge from the current direction + angle = stem.splitAngle(splitAng,splitAngV) + divRotMat = (Matrix()).Rotation(angle + stem.curv + uniform(-stem.curvV,stem.curvV),3,'X')#CurveUP should go after curve is applied + dirVec = zAxis.copy() + dirVec.rotate(divRotMat) + dirVec.rotate(splitRotMat(numSplit,i+1)) + dirVec.rotate(dir) + + # if attractUp != 0.0: # Shouldn't have a special case as this will mess with random number generation + divRotMat = (Matrix()).Rotation(angle + stem.curv + uniform(-stem.curvV,stem.curvV),3,'X') + dirVec = zAxis.copy() + dirVec.rotate(divRotMat) + dirVec.rotate(splitRotMat(numSplit,i+1)) + dirVec.rotate(dir) + + #Different version of the commented code above. We could use the inbuilt vector rotations but given this is a special case, it can be quicker to initialise the vector to the correct value. +# angle = stem.splitAngle(splitAng,splitAngV) +# curveUpAng = curveUp(attractUp,dir,stem.segMax) +# angleX = angle + stem.curv + uniform(-stem.curvV,stem.curvV) - curveUpAng +# angleZ = 2*i*pi/(numSplit+1) +# dirVec = Vector((sin(angleX)*sin(angleZ), -sin(angleX)*cos(angleZ), cos(angleX))) +# dirVec.rotate(dir) + + # Spread the stem out in a random fashion + spreadMat = (Matrix()).Rotation(spreadAng(degrees(dirVec.z)),3,'Z') + dirVec.rotate(spreadMat) + # Introduce upward curvature + upRotAxis = xAxis.copy() + upRotAxis.rotate(dirVec.to_track_quat('Z','Y')) + curveUpAng = curveUp(attractUp,dirVec.to_track_quat('Z','Y'),stem.segMax) + upRotMat = (Matrix()).Rotation(-curveUpAng,3,upRotAxis) + dirVec.rotate(upRotMat) + # Make the growth vec the length of a stem segment + dirVec.normalize() + dirVec *= stem.segL + # Add the new point and adjust its coords, handles and radius + newSpline.bezier_points.add() + newPoint = newSpline.bezier_points[-1] + (newPoint.co,newPoint.handle_left_type,newPoint.handle_right_type) = (stem.p.co + dirVec,hType,hType) + newPoint.radius = stem.radS*(1 - (stem.seg + 1)/stem.segMax) + stem.radE*((stem.seg + 1)/stem.segMax) + # If this isn't the last point on a stem, then we need to add it to the list of stems to continue growing + if stem.seg != stem.segMax: + splineList.append(stemSpline(newSpline,stem.curv-angle/(stem.segMax-stem.seg),stem.curvV,stem.seg+1,stem.segMax,stem.segL,stem.children,stem.radS,stem.radE,len(cu.splines)-1)) + splineToBone.append('bone'+(str(stem.splN)).rjust(3,'0')+'.'+(str(len(stem.spline.bezier_points)-2)).rjust(3,'0')) + # The original spline also needs to keep growing so adjust its direction too + angle = stem.splitAngle(splitAng,splitAngV) + divRotMat = (Matrix()).Rotation(angle + stem.curv + uniform(-stem.curvV,stem.curvV),3,'X') + dirVec = zAxis.copy() + dirVec.rotate(divRotMat) + dirVec.rotate(dir) + spreadMat = (Matrix()).Rotation(spreadAng(degrees(dirVec.z)),3,'Z') + dirVec.rotate(spreadMat) + else: + # If there are no splits then generate the growth direction without accounting for spreading of stems + dirVec = zAxis.copy() + #curveUpAng = curveUp(attractUp,dir,stem.segMax) + divRotMat = (Matrix()).Rotation(stem.curv + uniform(-stem.curvV,stem.curvV),3,'X') + dirVec.rotate(divRotMat) + #dirVec = Vector((0,-sin(stem.curv - curveUpAng),cos(stem.curv - curveUpAng))) + dirVec.rotate(dir) + upRotAxis = xAxis.copy() + upRotAxis.rotate(dirVec.to_track_quat('Z','Y')) + curveUpAng = curveUp(attractUp,dirVec.to_track_quat('Z','Y'),stem.segMax) + upRotMat = (Matrix()).Rotation(-curveUpAng,3,upRotAxis) + dirVec.rotate(upRotMat) + dirVec.normalize() + dirVec *= stem.segL + stem.spline.bezier_points.add() + newPoint = stem.spline.bezier_points[-1] + (newPoint.co,newPoint.handle_left_type,newPoint.handle_right_type) = (stem.p.co + dirVec,hType,hType) + newPoint.radius = stem.radS*(1 - (stem.seg + 1)/stem.segMax) + stem.radE*((stem.seg + 1)/stem.segMax) + # There are some cases where a point cannot have handles as VECTOR straight away, set these now. + if numSplit != 0: + tempPoint = stem.spline.bezier_points[-2] + (tempPoint.handle_left_type,tempPoint.handle_right_type) = ('VECTOR','VECTOR') + if len(stem.spline.bezier_points) == 2: + tempPoint = stem.spline.bezier_points[0] + (tempPoint.handle_left_type,tempPoint.handle_right_type) = ('VECTOR','VECTOR') + # Update the last point in the spline to be the newly added one + stem.updateEnd() + #return splineList + +def genLeafMesh(leafScale,leafScaleX,loc,quat,index,downAngle,downAngleV,rotate,rotateV,oldRot,bend,leaves): + verts = [Vector((0,0,0)),Vector((0.5,0,1/3)),Vector((0.5,0,2/3)),Vector((0,0,1)),Vector((-0.5,0,2/3)),Vector((-0.5,0,1/3))] + edges = [[0,1],[1,2],[2,3],[3,4],[4,5],[5,0],[0,3]] + faces = [[0,1,2,3],[0,3,4,5]] + #faces = [[0,1,5],[1,2,4,5],[2,3,4]] + + vertsList = [] + facesList = [] + + # If the special -ve flag is used we need a different rotation of the leaf geometry + if leaves < 0: + rotMat = (Matrix()).Rotation(oldRot,3,'Y') + oldRot += rotate/(abs(leaves)-1) + else: + oldRot += rotate+uniform(-rotateV,rotateV) + downRotMat = (Matrix()).Rotation(downAngle+uniform(-downAngleV,downAngleV),3,'X') + rotMat = (Matrix()).Rotation(oldRot,3,'Z') + + normal = yAxis.copy() + #dirVec = zAxis.copy() + orientationVec = zAxis.copy() + + # If the bending of the leaves is used we need to rotated them differently + if (bend != 0.0) and (leaves >= 0): +# normal.rotate(downRotMat) +# orientationVec.rotate(downRotMat) +# +# normal.rotate(rotMat) +# orientationVec.rotate(rotMat) + + normal.rotate(quat) + orientationVec.rotate(quat) + + thetaPos = atan2(loc.y,loc.x) + thetaBend = thetaPos - atan2(normal.y,normal.x) + rotateZ = (Matrix()).Rotation(bend*thetaBend,3,'Z') + normal.rotate(rotateZ) + orientationVec.rotate(rotateZ) + + phiBend = atan2((normal.xy).length,normal.z) + orientation = atan2(orientationVec.y,orientationVec.x) + rotateZOrien = (Matrix()).Rotation(orientation,3,'X') + + rotateX = (Matrix()).Rotation(bend*phiBend,3,'Z') + + rotateZOrien2 = (Matrix()).Rotation(-orientation,3,'X') + + # For each of the verts we now rotate and scale them, then append them to the list to be added to the mesh + for v in verts: + + v.z *= leafScale + v.x *= leafScaleX*leafScale + + if leaves > 0: + v.rotate(downRotMat) + + v.rotate(rotMat) + v.rotate(quat) + + if (bend != 0.0) and (leaves > 0): + # Correct the rotation + v.rotate(rotateZ) + v.rotate(rotateZOrien) + v.rotate(rotateX) + v.rotate(rotateZOrien2) + + #v.rotate(quat) + for v in verts: + v += loc + vertsList.append([v.x,v.y,v.z]) + + for f in faces: + facesList.append([f[0] + index,f[1] + index,f[2] + index,f[3] + index]) + return vertsList,facesList,oldRot + +def addTree(props): + global splitError + #startTime = time.time() + # Set the seed for repeatable results + seed(props.seed)# + + # Set all other variables + levels = props.levels# + length = props.length# + lengthV = props.lengthV# + branches = props.branches# + curveRes = props.curveRes# + curve = toRad(props.curve)# + curveV = toRad(props.curveV)# + curveBack = toRad(props.curveBack)# + baseSplits = props.baseSplits# + segSplits = props.segSplits# + splitAngle = toRad(props.splitAngle)# + splitAngleV = toRad(props.splitAngleV)# + scale = props.scale# + scaleV = props.scaleV# + attractUp = props.attractUp# + shape = int(props.shape)# + baseSize = props.baseSize + ratio = props.ratio + taper = props.taper# + ratioPower = props.ratioPower# + downAngle = toRad(props.downAngle)# + downAngleV = toRad(props.downAngleV)# + rotate = toRad(props.rotate)# + rotateV = toRad(props.rotateV)# + scale0 = props.scale0# + scaleV0 = props.scaleV0# + prune = props.prune# + pruneWidth = props.pruneWidth# + pruneWidthPeak = props.pruneWidthPeak# + prunePowerLow = props.prunePowerLow# + prunePowerHigh = props.prunePowerHigh# + pruneRatio = props.pruneRatio# + leafScale = props.leafScale# + leafScaleX = props.leafScaleX# + bend = props.bend# + leafDist = int(props.leafDist)# + bevelRes = props.bevelRes# + resU = props.resU# + useArm = props.useArm + + frameRate = props.frameRate + windSpeed = props.windSpeed + windGust = props.windGust + armAnim = props.armAnim + + leafObj = None + + # Some effects can be turned ON and OFF, the necessary variables are changed here + if not props.bevel: + bevelDepth = 0.0 + else: + bevelDepth = 1.0 + + if not props.showLeaves: + leaves = 0 + else: + leaves = props.leaves + + if props.handleType == '0': + handles = 'AUTO' + else: + handles = 'VECTOR' + + for ob in bpy.data.objects: + ob.select = False + + childP = [] + stemList = [] + + # Initialise the tree object and curve and adjust the settings + cu = bpy.data.curves.new('tree','CURVE') + treeOb = bpy.data.objects.new('tree',cu) + bpy.context.scene.objects.link(treeOb) + + cu.dimensions = '3D' + cu.use_fill_back = False + cu.use_fill_front = False + cu.bevel_depth = bevelDepth + cu.bevel_resolution = bevelRes + + # Fix the scale of the tree now + scaleVal = scale + uniform(-scaleV,scaleV) + + # If pruning is turned on we need to draw the pruning envelope + if prune: + enHandle = 'VECTOR' + enNum = 128 + enCu = bpy.data.curves.new('envelope','CURVE') + enOb = bpy.data.objects.new('envelope',enCu) + enOb.parent = treeOb + bpy.context.scene.objects.link(enOb) + newSpline = enCu.splines.new('BEZIER') + newPoint = newSpline.bezier_points[-1] + newPoint.co = Vector((0,0,scaleVal)) + (newPoint.handle_right_type,newPoint.handle_left_type) = (enHandle,enHandle) + # Set the coordinates by varying the z value, envelope will be aligned to the x-axis + for c in range(enNum): + newSpline.bezier_points.add() + newPoint = newSpline.bezier_points[-1] + ratioVal = (c+1)/(enNum) + zVal = scaleVal - scaleVal*(1-baseSize)*ratioVal + newPoint.co = Vector((scaleVal*pruneWidth*shapeRatio(8,ratioVal,pruneWidthPeak,prunePowerHigh,prunePowerLow),0,zVal)) + (newPoint.handle_right_type,newPoint.handle_left_type) = (enHandle,enHandle) + newSpline = enCu.splines.new('BEZIER') + newPoint = newSpline.bezier_points[-1] + newPoint.co = Vector((0,0,scaleVal)) + (newPoint.handle_right_type,newPoint.handle_left_type) = (enHandle,enHandle) + # Create a second envelope but this time on the y-axis + for c in range(enNum): + newSpline.bezier_points.add() + newPoint = newSpline.bezier_points[-1] + ratioVal = (c+1)/(enNum) + zVal = scaleVal - scaleVal*(1-baseSize)*ratioVal + newPoint.co = Vector((0,scaleVal*pruneWidth*shapeRatio(8,ratioVal,pruneWidthPeak,prunePowerHigh,prunePowerLow),zVal)) + (newPoint.handle_right_type,newPoint.handle_left_type) = (enHandle,enHandle) + + leafVerts = [] + leafFaces = [] + levelCount = [] + + splineToBone = deque(['']) + addsplinetobone = splineToBone.append + + # Each of the levels needed by the user we grow all the splines + for n in range(levels): + storeN = n + stemList = deque() + addstem = stemList.append + # If n is used as an index to access parameters for the tree it must be at most 3 or it will reference outside the array index + n = min(3,n) + vertAtt = attractUp + splitError = 0.0 + # If this is the first level of growth (the trunk) then we need some special work to begin the tree + if n == 0: + vertAtt = 0.0 + newSpline = cu.splines.new('BEZIER') + cu.resolution_u = resU + newPoint = newSpline.bezier_points[-1] + newPoint.co = Vector((0,0,0)) + newPoint.handle_right = Vector((0,0,1)) + newPoint.handle_left = Vector((0,0,-1)) + #(newPoint.handle_right_type,newPoint.handle_left_type) = ('VECTOR','VECTOR') + branchL = (scaleVal)*(length[0] + uniform(-lengthV[0],lengthV[0])) + childStems = branches[1] + startRad = branchL*ratio*(scale0 + uniform(-scaleV0,scaleV0)) + endRad = startRad*(1 - taper[0]) + newPoint.radius = startRad + addstem(stemSpline(newSpline,curve[0]/curveRes[0],curveV[0]/curveRes[0],0,curveRes[0],branchL/curveRes[0],childStems,startRad,endRad,0)) + # If this isn't the trunk then we may have multiple stem to intialise + else: + # Store the old rotation to allow new stems to be rotated away from the previous one. + oldRotate = 0 + # For each of the points defined in the list of stem starting points we need to grow a stem. + for p in childP: + # Add a spline and set the coordinate of the first point. + newSpline = cu.splines.new('BEZIER') + cu.resolution_u = resU + newPoint = newSpline.bezier_points[-1] + newPoint.co = p.co + tempPos = zAxis.copy() + # If the -ve flag for downAngle is used we need a special formula to find it + if downAngleV[n] < 0.0: + downV = downAngleV[n]*(1 - 2*shapeRatio(0,(p.lengthPar - p.offset)/(p.lengthPar - baseSize*scaleVal))) + random() + # Otherwise just find a random value + else: + downV = uniform(-downAngleV[n],downAngleV[n]) + downRotMat = (Matrix()).Rotation(downAngle[n]+downV,3,'X') + tempPos.rotate(downRotMat) + # If the -ve flag for rotate is used we need to find which side of the stem the last child point was and then grow in the opposite direction. + if rotate[n] < 0.0: + oldRotate = -copysign(rotate[n] + uniform(-rotateV[n],rotateV[n]),oldRotate) + # Otherwise just generate a random number in the specified range + else: + oldRotate += rotate[n]+uniform(-rotateV[n],rotateV[n]) + # Rotate the direction of growth and set the new point coordinates + rotMat = (Matrix()).Rotation(oldRotate,3,'Z') + tempPos.rotate(rotMat) + tempPos.rotate(p.quat) + newPoint.handle_right = p.co + tempPos + # If this is the first level of branching then upward attraction has no effect and a special formula is used to find branch length and the number of child stems + if n == 1: + vertAtt = 0.0 + lMax = length[1] + uniform(-lengthV[1],lengthV[1]) + branchL = p.lengthPar*lMax*shapeRatio(shape,(p.lengthPar - p.offset)/(p.lengthPar - baseSize*scaleVal)) + childStems = branches[2]*(0.2 + 0.8*(branchL/p.lengthPar)/lMax) + elif storeN <= levels - 2: + branchL = (length[n] + uniform(-lengthV[n],lengthV[n]))*(p.lengthPar - 0.6*p.offset) + childStems = branches[n+1]*(1.0 - 0.5*p.offset/p.lengthPar) + # If this is the last level before leaves then we need to generate the child points differently + else: + branchL = (length[n] + uniform(-lengthV[n],lengthV[n]))*(p.lengthPar - 0.6*p.offset) + if leaves < 0: + childStems = False + else: + childStems = leaves*shapeRatio(leafDist,p.offset/p.lengthPar) + # Determine the starting and ending radii of the stem using the tapering of the stem + startRad = min(p.radiusPar[0]*((branchL/p.lengthPar)**ratioPower), p.radiusPar[1]) + endRad = startRad*(1 - taper[n]) + newPoint.radius = startRad + # If curveBack is used then the curviness of the stem is different for the first half + if curveBack[n] == 0: + curveVal = curve[n]/curveRes[n] + else: + curveVal = 2*curve[n]/curveRes[n] + # Add the new stem to list of stems to grow and define which bone it will be parented to + addstem(stemSpline(newSpline,curveVal,curveV[n]/curveRes[n],0,curveRes[n],branchL/curveRes[n],childStems,startRad,endRad,len(cu.splines)-1)) + addsplinetobone(p.parBone) + + childP = [] + # Now grow each of the stems in the list of those to be extended + for st in stemList: + # When using pruning, we need to ensure that the random effects will be the same for each iteration to make sure the problem is linear. + randState = getstate() + startPrune = True + lengthTest = 0.0 + # Store all the original values for the stem to make sure we have access after it has been modified by pruning + originalLength = st.segL + originalCurv = st.curv + originalCurvV = st.curvV + originalSeg = st.seg + originalHandleR = st.p.handle_right.copy() + originalHandleL = st.p.handle_left.copy() + originalCo = st.p.co.copy() + currentMax = 1.0 + currentMin = 0.0 + currentScale = 1.0 + oldMax = 1.0 + deleteSpline = False + orginalSplineToBone = copy.copy(splineToBone) + forceSprout = False + # Now do the iterative pruning, this uses a binary search and halts once the difference between upper and lower bounds of the search are less than 0.005 + while startPrune and ((currentMax - currentMin) > 0.005): + setstate(randState) + + # If the search will halt after this iteration, then set the adjustment of stem length to take into account the pruning ratio + if (currentMax - currentMin) < 0.01: + currentScale = (currentScale - 1)*pruneRatio + 1 + startPrune = False + forceSprout = True + # Change the segment length of the stem by applying some scaling + st.segL = originalLength*currentScale + # To prevent millions of splines being created we delete any old ones and replace them with only their first points to begin the spline again + if deleteSpline: + for x in splineList: + cu.splines.remove(x.spline) + newSpline = cu.splines.new('BEZIER') + newPoint = newSpline.bezier_points[-1] + newPoint.co = originalCo + newPoint.handle_right = originalHandleR + newPoint.handle_left = originalHandleL + (newPoint.handle_left_type,newPoint.handle_right_type) = ('VECTOR','VECTOR') + st.spline = newSpline + st.curv = originalCurv + st.curvV = originalCurvV + st.seg = originalSeg + st.p = newPoint + newPoint.radius = st.radS + splineToBone = orginalSplineToBone + + # Initialise the spline list for those contained in the current level of branching + splineList = [st] + # For each of the segments of the stem which must be grown we have to add to each spline in splineList + for k in range(curveRes[n]): + # Make a copy of the current list to avoid continually adding to the list we're iterating over + tempList = splineList[:] + #print('Leng: ',len(tempList)) + # For each of the splines in this list set the number of splits and then grow it + for spl in tempList: + if k == 0: + numSplit = 0 + elif (k == 1) and (n == 0): + numSplit = baseSplits + else: + numSplit = splits(segSplits[n]) + if (k == int(curveRes[n]/2)) and (curveBack[n] != 0): + spl.curvAdd(-2*curve[n]/curveRes[n] + 2*curveBack[n]/curveRes[n]) + growSpline(spl,numSplit,splitAngle[n],splitAngleV[n],splineList,vertAtt,handles,splineToBone)# Add proper refs for radius and attractUp + + # If pruning is enabled then we must to the check to see if the end of the spline is within the evelope + if prune: + # Check each endpoint to see if it is inside + for s in splineList: + coordMag = (s.spline.bezier_points[-1].co.xy).length + ratio = (scaleVal - s.spline.bezier_points[-1].co.z)/(scaleVal*(1 - baseSize)) + # Don't think this if part is needed + if (n == 0) and (s.spline.bezier_points[-1].co.z < baseSize*scaleVal): + pass#insideBool = True + else: + insideBool = ((coordMag/scaleVal) < pruneWidth*shapeRatio(8,ratio,pruneWidthPeak,prunePowerHigh,prunePowerLow)) + # If the point is not inside then we adjust the scale and current search bounds + if not insideBool: + oldMax = currentMax + currentMax = currentScale + currentScale = 0.5*(currentMax + currentMin) + break + # If the scale is the original size and the point is inside then we need to make sure it won't be pruned or extended to the edge of the envelope + if insideBool and (currentScale != 1): + currentMin = currentScale + currentMax = oldMax + currentScale = 0.5*(currentMax + currentMin) + if insideBool and ((currentMax - currentMin) == 1): + currentMin = 1 + # If the search will halt on the next iteration then we need to make sure we sprout child points to grow the next splines or leaves + if (((currentMax - currentMin) < 0.005) or not prune) or forceSprout: + tVals = findChildPoints(splineList,st.children) + # If leaves is -ve then we need to make sure the only point which sprouts is the end of the spline + #if not st.children: + if not st.children: + tVals = [0.9] + # If this is the trunk then we need to remove some of the points because of baseSize + if n == 0: + trimNum = int(baseSize*(len(tVals)+1)) + tVals = tVals[trimNum:] + + # For all the splines, we interpolate them and add the new points to the list of child points + for s in splineList: + #print(str(n)+'level: ',s.segMax*s.segL) + childP.extend(interpStem(s,tVals,s.segMax*s.segL,s.radS)) + + # Force the splines to be deleted + deleteSpline = True + # If pruning isn't enabled then make sure it doesn't loop + if not prune: + startPrune = False + + levelCount.append(len(cu.splines)) + # If we need to add leaves, we do it here + if (storeN == levels-1) and leaves: + oldRot = 0.0 + n = min(3,n+1) + # For each of the child points we add leaves + for cp in childP: + # If the special flag is set then we need to add several leaves at the same location + if leaves < 0: + oldRot = -rotate[n]/2 + for g in range(abs(leaves)): + (vertTemp,faceTemp,oldRot) = genLeafMesh(leafScale,leafScaleX,cp.co,cp.quat,len(leafVerts),downAngle[n],downAngleV[n],rotate[n],rotateV[n],oldRot,bend,leaves) + leafVerts.extend(vertTemp) + leafFaces.extend(faceTemp) + # Otherwise just add the leaves like splines. + else: + (vertTemp,faceTemp,oldRot) = genLeafMesh(leafScale,leafScaleX,cp.co,cp.quat,len(leafVerts),downAngle[n],downAngleV[n],rotate[n],rotateV[n],oldRot,bend,leaves) + leafVerts.extend(vertTemp) + leafFaces.extend(faceTemp) + # Create the leaf mesh and object, add geometry using from_pydata, edges are currently added by validating the mesh which isn't great + leafMesh = bpy.data.meshes.new('leaves') + leafObj = bpy.data.objects.new('leaves',leafMesh) + bpy.context.scene.objects.link(leafObj) + leafObj.parent = treeOb + leafMesh.from_pydata(leafVerts,(),leafFaces) + leafMesh.validate() + +# This can be used if we need particle leaves +# if (storeN == levels-1) and leaves: +# normalList = [] +# oldRot = 0.0 +# n = min(3,n+1) +# oldRot = 0.0 +# # For each of the child points we add leaves +# for cp in childP: +# # Here we make the new "sprouting" stems diverge from the current direction +# dirVec = zAxis.copy() +# oldRot += rotate[n]+uniform(-rotateV[n],rotateV[n]) +# downRotMat = (Matrix()).Rotation(downAngle[n]+uniform(-downAngleV[n],downAngleV[n]),3,'X') +# rotMat = (Matrix()).Rotation(oldRot,3,'Z') +# dirVec.rotate(downRotMat) +# dirVec.rotate(rotMat) +# dirVec.rotate(cp.quat) +# normalList.extend([dirVec.x,dirVec.y,dirVec.z]) +# leafVerts.append(cp.co) +# # Create the leaf mesh and object, add geometry using from_pydata, edges are currently added by validating the mesh which isn't great +# edgeList = [(a,a+1) for a in range(len(childP)-1)] +# leafMesh = bpy.data.meshes.new('leaves') +# leafObj = bpy.data.objects.new('leaves',leafMesh) +# bpy.context.scene.objects.link(leafObj) +# leafObj.parent = treeOb +# leafMesh.from_pydata(leafVerts,edgeList,()) +# leafMesh.vertices.foreach_set('normal',normalList) + + # If we need and armature we add it + if useArm: + # Create the armature and objects + arm = bpy.data.armatures.new('tree') + armOb = bpy.data.objects.new('treeArm',arm) + bpy.context.scene.objects.link(armOb) + + # Create a new action to store all animation + newAction = bpy.data.actions.new(name='windAction') + armOb.animation_data_create() + armOb.animation_data.action = newAction + + arm.draw_type = 'STICK' + arm.use_deform_delay = True + + # Add the armature modifier to the curve + armMod = treeOb.modifiers.new('windSway','ARMATURE') + #armMod.use_apply_on_spline = True + armMod.object = armOb + + # If there are leaves then they need a modifier + if leaves: + armMod = leafObj.modifiers.new('windSway','ARMATURE') + armMod.object = armOb + + # Make sure all objects are deselected (may not be required?) + for ob in bpy.data.objects: + ob.select = False + + # Set the armature as active and go to edit mode to add bones + bpy.context.scene.objects.active = armOb + bpy.ops.object.mode_set(mode='EDIT') + + masterBones = [] + + offsetVal = 0 + + # For all the splines in the curve we need to add bones at each bezier point + for i,parBone in enumerate(splineToBone): + s = cu.splines[i] + b = None + # Get some data about the spline like length and number of points + numPoints = len(s.bezier_points)-1 + splineL = numPoints*((s.bezier_points[0].co-s.bezier_points[1].co).length) + # Set the random phase difference of the animation + bxOffset = uniform(0,2*pi) + byOffset = uniform(0,2*pi) + # Set the phase multiplier for the spline + bMult = (s.bezier_points[0].radius/splineL)*(1/15)*(1/frameRate) + # For all the points in the curve (less the last) add a bone and name it by the spline it will affect + for n in range(numPoints): + oldBone = b + boneName = 'bone'+(str(i)).rjust(3,'0')+'.'+(str(n)).rjust(3,'0') + b = arm.edit_bones.new(boneName) + b.head = s.bezier_points[n].co + b.tail = s.bezier_points[n+1].co + + b.head_radius = s.bezier_points[n].radius + b.tail_radius = s.bezier_points[n+1].radius + b.envelope_distance = 0.001#0.001 + + # If there are leaves then we need a new vertex group so they will attach to the bone + if (len(levelCount) > 1) and (i >= levelCount[-2]) and leafObj: + leafObj.vertex_groups.new(boneName) + elif (len(levelCount) == 1) and leafObj: + leafObj.vertex_groups.new(boneName) + # If this is first point of the spline then it must be parented to the level above it + if n == 0: + if parBone: + b.parent = arm.edit_bones[parBone] +# if len(parBone) > 11: +# b.use_connect = True + # Otherwise, we need to attach it to the previous bone in the spline + else: + b.parent = oldBone + b.use_connect = True + # If there isn't a previous bone then it shouldn't be attached + if not oldBone: + b.use_connect = False + #tempList.append(b) + + # Add the animation to the armature if required + if armAnim: + # Define all the required parameters of the wind sway by the dimension of the spline + a0 = 4*splineL*(1-n/(numPoints+1))/s.bezier_points[n].radius + a1 = (windSpeed/50)*a0 + a2 = (windGust/50)*a0 + a1/2 + + # Add new fcurves for each sway as well as the modifiers + swayX = armOb.animation_data.action.fcurves.new('pose.bones["' + boneName + '"].rotation_euler',0) + swayY = armOb.animation_data.action.fcurves.new('pose.bones["' + boneName + '"].rotation_euler',2) + + swayXMod1 = swayX.modifiers.new(type='FNGENERATOR') + swayXMod2 = swayX.modifiers.new(type='FNGENERATOR') + + swayYMod1 = swayY.modifiers.new(type='FNGENERATOR') + swayYMod2 = swayY.modifiers.new(type='FNGENERATOR') + + # Set the parameters for each modifier + swayXMod1.amplitude = radians(a1)/numPoints + swayXMod1.phase_offset = bxOffset + swayXMod1.phase_multiplier = degrees(bMult) + + swayXMod2.amplitude = radians(a2)/numPoints + swayXMod2.phase_offset = 0.7*bxOffset + swayXMod2.phase_multiplier = 0.7*degrees(bMult) # This shouldn't have to be in degrees but it looks much better in animation + swayXMod2.use_additive = True + + swayYMod1.amplitude = radians(a1)/numPoints + swayYMod1.phase_offset = byOffset + swayYMod1.phase_multiplier = degrees(bMult) # This shouldn't have to be in degrees but it looks much better in animation + + swayYMod2.amplitude = radians(a2)/numPoints + swayYMod2.phase_offset = 0.7*byOffset + swayYMod2.phase_multiplier = 0.7*degrees(bMult) # This shouldn't have to be in degrees but it looks much better in animation + swayYMod2.use_additive = True + + # If there are leaves we need to assign vertices to their vertex groups + if leaves: + offsetVal = 0 + for i,cp in enumerate(childP): + for v in leafMesh.vertices[6*i:(6*i+6)]: + leafObj.vertex_groups[cp.parBone].add([v.index],1.0,'ADD') + + # Now we need the rotation mode to be 'XYZ' to ensure correct rotation + bpy.ops.object.mode_set(mode='OBJECT') + for p in armOb.pose.bones: + p.rotation_mode = 'XYZ' + treeOb.parent = armOb + #print(time.time()-startTime) \ No newline at end of file