Newer
Older
#
# 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.
#
"name": "Sapling Tree Gen",
"author": "Andrew Hale (TrumanBlending), Aaron Buchler, CansecoGPC",
"version": (0, 3, 4),
"blender": (2, 80, 0),
"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 "
"doc_url": "{BLENDER_MANUAL_URL}/addons/add_curve/sapling.html",
"category": "Add Curve",
}
CoDEmanX
committed
import importlib
importlib.reload(utils)
else:
from add_curve_sapling import utils
import bpy
import time
import os
# import cProfile
from bpy.types import (
Operator,
Menu,
)
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
FloatVectorProperty,
IntProperty,
IntVectorProperty,
StringProperty,
)
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')]
shapeList3 = [('0', 'Conical', ''),
('6', 'Inverse Conical', ''),
('1', 'Spherical', ''),
('2', 'Hemispherical', ''),
('3', 'Cylindrical', ''),
('4', 'Tapered Cylindrical', ''),
('10', 'Inverse Tapered Cylindrical', ''),
('5', 'Flame', ''),
('7', 'Tend Flame', ''),
('8', 'Custom Shape', '')]
shapeList4 = [('0', 'Conical', ''),
('6', 'Inverse Conical', ''),
('1', 'Spherical', ''),
('2', 'Hemispherical', ''),
('3', 'Cylindrical', ''),
('4', 'Tapered Cylindrical', ''),
('10', 'Inverse Tapered Cylindrical', ''),
('5', 'Flame', ''),
('7', 'Tend Flame', '')]
handleList = [('0', 'Auto', 'Auto'),
('1', 'Vector', 'Vector')]
settings = [('0', 'Geometry', 'Geometry'),
('1', 'Branch Radius', 'Branch Radius'),
('2', 'Branch Splitting', 'Branch Splitting'),
('3', 'Branch Growth', 'Branch Growth'),
('4', 'Pruning', 'Pruning'),
('5', 'Leaves', 'Leaves'),
('6', 'Armature', 'Armature'),
('7', 'Animation', 'Animation')]
branchmodes = [("original", "Original", "rotate around each branch"),
("rotate", "Rotate", "evenly distribute branches to point outward from center of tree"),
("random", "Random", "choose random point")]
"""Support user defined scripts directory
Find the first occurrence of add_curve_sapling/presets in possible script paths
and return it as preset path"""
script_file = os.path.realpath(__file__)
directory = os.path.dirname(script_file)
directory = os.path.join(directory, "presets")
return directory
def getPresetpaths():
"""Return paths for both local and user preset folders"""
userDir = os.path.join(bpy.utils.script_path_user(), 'presets', 'operator', 'add_curve_sapling')
if os.path.isdir(userDir):
pass
else:
os.makedirs(userDir)
script_file = os.path.realpath(__file__)
directory = os.path.dirname(script_file)
localDir = os.path.join(directory, "presets")
return (localDir, userDir)
"""This operator handles writing presets to file"""
bl_idname = 'sapling.exportdata'
bl_label = 'Export Preset'
def execute(self, context):
# Unpack some data from the input
data, filename, overwrite = eval(self.data)
"""
try:
# Check whether the file exists by trying to open it.
f = open(os.path.join(getPresetpaths()[1], 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(getPresetpaths()[1], filename + '.py'), 'w')
f.write(data)
f.close()
return {'FINISHED'}
else:
return {'CANCELLED'}
"""
fpath1 = os.path.join(getPresetpaths()[0], filename + '.py')
fpath2 = os.path.join(getPresetpaths()[1], filename + '.py')
if os.path.exists(fpath1):
# If it exists in built-in presets then report an error
self.report({'ERROR_INVALID_INPUT'}, 'Can\'t have same name as built-in preset')
elif (not os.path.exists(fpath2)) or (os.path.exists(fpath2) and overwrite):
# if (it does not exist) or (exists and overwrite) then write file
if data:
# If it doesn't exist, create the file with the required data
f = open(os.path.join(getPresetpaths()[1], filename + '.py'), 'w')
f.write(data)
f.close()
return {'FINISHED'}
else:
return {'CANCELLED'}
else:
# If it exists then report an error
self.report({'ERROR_INVALID_INPUT'}, 'Preset Already Exists')
return {'CANCELLED'}
"""This operator handles importing existing presets"""
bl_idname = "sapling.importdata"
bl_label = "Import Preset"
def execute(self, context):
# Make sure the operator knows about the global variables
global settings, useSet
# Read the preset data into the global settings
try:
f = open(os.path.join(getPresetpaths()[0], self.filename), 'r')
except (FileNotFoundError, IOError):
f = open(os.path.join(getPresetpaths()[1], self.filename), 'r')
settings = ast.literal_eval(settings)
if type(settings['attractUp']) == float:
atr = settings['attractUp']
settings['attractUp'] = [0, 0, atr, atr]
if 'leafDownAngle' not in settings:
l = settings['levels']
settings['leafDownAngle'] = settings['downAngle'][min(l, 3)]
settings['leafDownAngleV'] = settings['downAngleV'][min(l, 3)]
settings['leafRotate'] = settings['rotate'][min(l, 3)]
settings['leafRotateV'] = settings['rotateV'][min(l, 3)]
# Set the flag to use the settings
useSet = True
return {'FINISHED'}
"""Create the preset menu by finding all preset files
in the preset directory"""
bl_idname = "SAPLING_MT_preset"
bl_label = "Presets"
def draw(self, context):
# Get all the sapling presets
presets = [a for a in os.listdir(getPresetpaths()[0]) if a[-3:] == '.py']
presets.extend([a for a in os.listdir(getPresetpaths()[1]) 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
Andrew Hale
committed
bl_label = "Sapling: Add Tree"
def objectList(self, context):
objects = []
bObjects = bpy.data.objects
for obj in bObjects:
if (obj.type in ['MESH', 'CURVE', 'SURFACE']) and (obj.name not in ['tree', 'leaves']):
objects.append((obj.name, obj.name, ""))
return (objects if objects else
[('NONE', "No objects", "No appropriate objects in the Scene")])
def update_tree(self, context):
self.do_update = True
CoDEmanX
committed
def update_leaves(self, context):
if self.showLeaves:
self.do_update = True
else:
self.do_update = False
def no_update_tree(self, context):
self.do_update = False
name='Do Update',
default=True, options={'HIDDEN'}
)
description='Choose the settings to modify',
items=settings,
default='0', update=no_update_tree
)
default=False, update=update_tree
)
default=False, update=update_tree
)
default=False, update=update_tree
)
default=False, update=update_tree
)
description='The seed of the random number generator',
description='The type of curve handles',
min=0,
max=1,
description='Number of recursive branches (Levels)',
description='The relative lengths of each branch level (nLength)',
description='The relative length variations of each level (nLengthV)',
min=0.0,
description='Shorten trunk splits toward outside of tree',
min=0.0,
soft_max=1.0,
description='The number of branches grown at each level (nBranches)',
min=0,
default=[50, 30, 10, 10],
description='The number of segments on each branch (nCurveRes)',
min=1,
default=[3, 5, 3, 1],
description='The angle of the end of the branch (nCurve)',
default=[0, -40, -40, 0],
description='Variation of the curvature (nCurveV)',
default=[20, 50, 75, 0],
curveBack: FloatVectorProperty(
description='Curvature for the second half of a branch (nCurveBack)',
default=[0, 0, 0, 0],
description='Number of trunk splits at its base (nBaseSplits)',
min=0,
segSplits: FloatVectorProperty(
description='Number of splits per segment (nSegSplits)',
min=0,
description='Split proportional to branch length',
default=False, update=update_tree
)
description='Branching and Rotation Mode',
items=branchmodes,
default="rotate", update=update_tree
)
splitAngle: FloatVectorProperty(
description='Angle of branch splitting (nSplitAngle)',
default=[0, 0, 0, 0],
splitAngleV: FloatVectorProperty(
description='Variation in the split angle (nSplitAngleV)',
default=[0, 0, 0, 0],
scaleV: FloatProperty(name='Scale Variation',
description='The variation in the tree scale (ScaleV)',
default=3.0, update=update_tree
)
attractUp: FloatVectorProperty(
attractOut: FloatVectorProperty(
description='Branch outward attraction',
default=[0, 0, 0, 0],
min=0.0,
max=1.0,
description='The overall shape of the tree (Shape)',
items=shapeList3,
default='7', update=update_tree
)
description='The shape of secondary splits',
items=shapeList4,
default='4', update=update_tree
)
customShape: FloatVectorProperty(
description='custom shape branch length at (Base, Middle, Middle Position, Top)',
size=4,
min=.01,
max=1,
default=[.5, 1.0, .3, .5], update=update_tree
)
description='Adjust branch spacing to put more branches at the top or bottom of the tree',
min=0.1,
soft_max=10,
default=1.0, update=update_tree
)
description='grow branches in rings',
min=0,
description='Fraction of tree height with no branches (Base Size)',
min=0.0,
default=0.4, update=update_tree
)
description='Factor to decrease base size for each level',
min=0.0,
max=1.0,
default=0.25, update=update_tree
)
description='Fraction of tree height with no splits',
min=0.0,
max=1.0,
default=0.2, update=update_tree
)
description='Put more splits at the top or bottom of the tree',
soft_min=-2.0,
soft_max=2.0,
default=0.0, update=update_tree
)
description='Base radius size (Ratio)',
min=0.0,
default=0.015, update=update_tree
)
description='Minimum branch Radius',
min=0.0,
default=0.0, update=update_tree
)
description='Set radius at branch tips to zero',
default=False, update=update_tree
)
description='Root radius factor',
min=1.0,
default=1.0, update=update_tree
)
description='Calculate taper automatically based on branch lengths',
default=True, update=update_tree
)
description='The fraction of tapering on each branch (nTaper)',
min=0.0,
max=1.0,
default=[1, 1, 1, 1],
radiusTweak: FloatVectorProperty(
description='multiply radius by this factor',
min=0.0,
max=1.0,
default=[1, 1, 1, 1],
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, update=update_tree
)
downAngle: FloatVectorProperty(
description=('The angle between a new branch and the one it grew '
'from (nDownAngle)'),
default=[90, 60, 45, 45],
downAngleV: FloatVectorProperty(
name='Down Angle Variation',
description="Angle to decrease Down Angle by towards end of parent branch "
"(negative values add random variation)",
useOldDownAngle: BoolProperty(
name='Use old down angle variation',
default=False, update=update_tree
)
name='Use parent angle',
description='(first level) Rotate branch to match parent branch',
default=True, update=update_tree
)
name='Rotate Angle',
description="The angle of a new branch around the one it grew from "
"(negative values rotate opposite from the previous)",
default=[137.5, 137.5, 137.5, 137.5],
description='Variation in the rotate angle (nRotateV)',
default=[0, 0, 0, 0],
description='The scale of the trunk radius (0Scale)',
min=0.0,
default=1.0, update=update_tree
)
description='Variation in the radius scale (0ScaleV)',
default=0.2, update=update_tree
)
description='The width of the envelope (PruneWidth)',
min=0.0,
default=0.4, update=update_tree
)
description='The height of the base of the envelope, bound by trunk height',
min=0.0,
max=1.0,
default=0.3, update=update_tree
)
pruneWidthPeak: FloatProperty(
name='Prune Width Peak',
description=("Fraction of envelope height where the maximum width "
"occurs (PruneWidthPeak)"),
default=0.6, update=update_tree
)
prunePowerHigh: FloatProperty(
description=('Power which determines the shape of the upper portion '
'of the envelope (PrunePowerHigh)'),
default=0.5, update=update_tree
)
description=('Power which determines the shape of the lower portion '
'of the envelope (PrunePowerLow)'),
default=0.001, update=update_tree
)
description='Proportion of pruned length (PruneRatio)',
min=0.0,
max=1.0,
default=1.0, update=update_tree
)
name='Leaves',
description="Maximum number of leaves per branch (negative values grow "
"leaves from branch tip (palmate compound leaves))",
default=25, update=update_tree
)
description='The angle between a new leaf and the branch it grew from',
default=45, update=update_leaves
)
leafDownAngleV: FloatProperty(
name='Leaf Down Angle Variation',
description="Angle to decrease Down Angle by towards end of parent branch "
"(negative values add random variation)",
default=10, update=update_tree
)
name='Leaf Rotate Angle',
description="The angle of a new leaf around the one it grew from "
"(negative values rotate opposite from previous)",
default=137.5, update=update_tree
)
description='Variation in the rotate angle',
default=0.0, update=update_leaves
)
description='The scaling applied to the whole leaf (LeafScale)',
min=0.0,
default=0.17, update=update_leaves
)
description=('The scaling applied to the x direction of the leaf '
'(LeafScaleX)'),
min=0.0,
default=1.0, update=update_leaves
)
description='scale leaves toward the tip or base of the patent branch',
min=-1.0,
max=1.0,
default=0.0, update=update_leaves
)
description='randomize leaf scale',
min=0.0,
max=1.0,
default=0.0, update=update_leaves
)
description='The shape of the leaves',
items=(('hex', 'Hexagonal', '0'), ('rect', 'Rectangular', '1'),
('dFace', 'DupliFaces', '2'), ('dVert', 'DupliVerts', '3')),
default='hex', update=update_leaves
)
description='Object to use for leaf instancing if Leaf Shape is DupliFaces or DupliVerts',
items=objectList,
update=update_leaves
)
"""
bend = FloatProperty(
name='Leaf Bend',
description='The proportion of bending applied to the leaf (Bend)',
min=0.0,
max=1.0,
default=0.0, update=update_leaves
)
"""
description='Leaf vertical attraction',
default=0.0, update=update_leaves
)
description='Leaves face upwards',
default=True, update=update_leaves
)
description='The way leaves are distributed on branches',
default='6', update=update_tree
)
description='The bevel resolution of the curves',
min=0,
description='The resolution along the curves',
min=1,
description='The type of handles used in the spline',
items=handleList,
default='0', update=update_tree
)
description='Whether animation is added to the armature',
default=False, update=update_tree
)
description='Disable armature modifier, hide tree, and set bone display to wire, for fast playback',
# Disable skin modifier and hide tree and armature, for fast playback
default=False, update=update_tree
)
description='Whether animation is added to the leaves',
default=False, update=update_tree
)
description=('Adjust speed of animation, relative to scene frame rate'),
min=0.001,
description='Number of frames to make the animation loop for, zero is disabled',
min=0,
default=0, update=update_tree
)
"""
windSpeed = FloatProperty(
name='Wind Speed',
description='The wind speed to apply to the armature',
default=2.0, update=update_tree
)
windGust = FloatProperty(
name='Wind Gust',
description='The greatest increase over Wind Speed',
default=0.0, update=update_tree
)
"""
description='The intensity of the wind to apply to the armature',
default=1.0, update=update_tree
)
description='The amount of directional movement, (from the positive Y direction)',
default=1.0, update=update_tree
)
description='The Frequency of directional movement',
default=0.075, update=update_tree
)
description='Multiplier for noise amplitude',
default=1.0, update=update_tree
)
description='Multiplier for noise fequency',
default=1.0, update=update_tree
)
description='Random offset in noise',
default=4.0, update=update_tree
)
description='Convert curves to mesh, uses skin modifier, enables armature simplification',
default=False, update=update_tree
)
description='Number of branching levels to make bones for, 0 is all levels',
min=0,
description='Number of stem segments per bone',
min=1,
default=[1, 1, 1, 1],
description='The name of the preset to be saved',
default='',
subtype='FILE_NAME', update=no_update_tree
)
description='Limited imported tree to 2 levels & no leaves for speed',
default=True, update=no_update_tree
)
description='When checked, overwrite existing preset files when saving',
default=False, update=no_update_tree
)
"""
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, update=update_tree
)
"""
@classmethod
def poll(cls, context):
return context.mode == 'OBJECT'
def draw(self, context):
# layout.label(text='Tree Definition')
CoDEmanX
committed
row = box.row()
row.prop(self, 'bevelRes')
row.prop(self, 'resU')
CoDEmanX
committed
box.prop(self, 'shape')
col = box.column()
col.prop(self, 'customShape')
row = box.row()
box.prop(self, 'shapeS')
box.prop(self, 'branchDist')
box.prop(self, 'nrings')
CoDEmanX
committed
row.prop(self, 'scale')
row.prop(self, 'scaleV')
CoDEmanX
committed
# 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=("chooseSet", "presetName", "limitImport",
"do_update", "overwrite", "leafDupliObj"))).items():
# If the property is a vector property then add the slice to the list
try:
len(b)
data.append((a, b[:]))
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, self.overwrite])
row = box.row()
row.prop(self, 'overwrite')
row.menu('SAPLING_MT_preset', text='Load Preset')
box.label(text="Branch Radius:")
row = box.row()
row.prop(self, 'bevel')
row.prop(self, 'bevelRes')
box.prop(self, 'ratio')
row = box.row()
row.prop(self, 'scale0')
row.prop(self, 'scaleV0')
box.prop(self, 'ratioPower')
box.prop(self, 'minRadius')
box.prop(self, 'closeTip')
box.prop(self, 'rootFlare')
box.prop(self, 'autoTaper')
split = box.split()
col = split.column()
col.prop(self, 'taper')
col = split.column()
col.prop(self, 'radiusTweak')
elif self.chooseSet == '2':
box.label(text="Branch Splitting:")
box.prop(self, 'levels')
box.prop(self, 'baseSplits')
row = box.row()
row.prop(self, 'baseSize')
row.prop(self, 'baseSize_s')
box.prop(self, 'splitHeight')
box.prop(self, 'splitBias')
box.prop(self, 'splitByLen')
CoDEmanX
committed
CoDEmanX
committed
col.prop(self, 'branches')
col.prop(self, 'splitAngle')
col.prop(self, 'rotate')
col.prop(self, 'attractOut')
CoDEmanX
committed
col = split.column()
col.prop(self, 'segSplits')
col.prop(self, 'splitAngleV')
col.label(text="Branching Mode:")
box.column().prop(self, 'curveRes')
elif self.chooseSet == '3':
box.label(text="Branch Growth:")
box.prop(self, 'taperCrown')
CoDEmanX
committed
CoDEmanX
committed
col.prop(self, 'downAngle')
col.prop(self, 'curve')
col.prop(self, 'curveBack')
CoDEmanX
committed
col = split.column()
col.prop(self, 'lengthV')