diff --git a/README.rst b/README.rst index 78fe0c2..b4b8e8f 100644 --- a/README.rst +++ b/README.rst @@ -2,24 +2,271 @@ TreeSim_Lpy ============ +Description +----------- + +TreeSim_Lpy is a tree modelling tool which is built upon L-py with the added features of pruning and tying trees down to mimic different architectures. The tool uses python and prior knowledge of L-systems and L-Py is needed to work with this tool. This tool is ideal for researchers and developers working on botanical simulations and robotic harvesting applications. -TreeSim_Lpy is a tree modelling tool which is built upon L-py with the added features of pruning -and tying trees down to mimic different architectures. The tool uses python and prior knowledge of L-systems -and L-Py is needed to work with this tool. Python version 3.9 +Table of Contents +----------------- + +- `Installation <#installation>`__ +- `Tutorials <#tutorials>`__ +- `Usage <#usage>`__ +- `Features <#features>`__ +- `Contributing <#contributing>`__ +- `License <#license>`__ +- `Contact <#contact>`__ +- `Acknowledgments <#acknowledgments>`__ + +Documentation +------------- + +The documentation is provided at `Read the Docs `__. + +You can find the latest L-Py documentation at + + +Installation +------------ + +To install TreeSim_Lpy, follow these steps (adapted from the `L-Py documentation `__): + +1. **Install Conda**: + + The L-Py distribution relies on the conda software environment management system. If you do not already have conda installed, you can find installation instructions on the `Conda Installation Page `__. + +2. **Create a Conda Environment**: + + Create an environment named `lpy`: + + .. code-block:: sh + + conda create -n lpy openalea.lpy -c fredboudon -c conda-forge + + The package is retrieved from the `fredboudon` channel (development), and its dependencies will be taken from the `conda-forge` channel. + +3. **Activate the L-Py Environment**: + + .. code-block:: sh + + conda activate lpy + +4. **Install Required Packages**: + + .. code-block:: sh + + pip install -r requirements.txt + +5. **Run L-Py**: + + .. code-block:: sh + + lpy + + +Tutorials +--------- + +There are many things you may want to modify as you grow your own trees. Here are some tutorials for some of the more common changes: + +1. **Changing Apple Geometry:** + + The call production of the apples happens in the ``grow_object(o)`` section: + + .. code-block:: python + + elif 'Apple' in o.name: + produce [S(.1/2, .09/15)f(.1)&(180)A(.1, .09)] + + + + The apple's base is generated with the ``A(bh, r)`` production rule seen below. + + .. code-block:: python + + A(bh, r): + curves = make_apple_curve() + base_curve = curves[0] + top_curve = curves[1] + nproduce SetColor(230,0,0) SectionResolution(60) + produce nF(bh, .01, r, base_curve) ^(180) nF(bh/5, .1, r, top_curve)^(180) + + The parameters represent the base height of the apple and the radius of the apple. If you wanted to create a completely new apple geometry, just replace the code in this A section. However, if you simply want to edit the existing shape of the apple, that can be done in the ``make_apple_curve()`` section. + + The apple is made with two curves: a curve that marks the base of the apple, and a curve that marks the indentation on top of the apple. These curves are generated as different Curve2D objects, then turned into QuantisedFunction objects. This is necessary because of the way the apple is produced ``nF``. ``nF`` has an optional parameter ``radiusvariation`` which must be a quantized function. ``nF`` produces a cylinder in n steps, and these curves work by specifying how large the radius for the cylinder should be at each step. + + Currently, the stem is produced separately from the apple base. The stem is created in a slightly different way than the apple. A NurbsCurve2D object is returned from the ``make_stem_curve()`` function. This curve is used in ``SetGuide`` to mark how the stem will be generated. ``nF`` is used to follow the guide while generating a cylinder, and there is no ``radiusvariation`` this time. + + .. code-block:: python + + S(sh,r): + stem_curve = make_stem_curve() + nproduce SetColor(100,65,23) + produce SetGuide(stem_curve, sh) _(r)nF(sh, .1, r) + + +2. **Changing Leaf Geometry:** + + The call production of the leaves happens in the ``grow_object(o)`` section: + + .. code-block:: python + + elif 'Leaf' in o.name: + produce L(.1) + + Here, .1 is just a hard-coded value that doesn't have much significance. + + The actual generation of the leaf can be seen in the ``L(l)`` production section: + + .. code-block:: python + + L(l): + nproduce SetColor(0,225,0) + + curves = make_leaf_guide() + curve1 = curves[0] + curve2 = curves[1] + produce _(.0025) F(l/10){[SetGuide(curve1, l) _(.001).nF(l, .01)][SetGuide(curve2, l)_(.001).nF(l, .01)]} + + The parameter here serves as the length of the leaf. To edit the shape of the leaf, edits can be made in the ``make_leaf_guide()`` function. In the ``make_leaf_guide()`` function, a BezierCurve2D and its inverse are generated. These are returned to be used as guides for the ``SetGuide`` function provided by L-Py. These curves are generated with random points from a set range in order to create leaves of different shape. These randomness of these points, or the range they are generated from, could be edited to change the leaf shape. However, a new geometry altogether could be made in the ``L(l)`` section. + + + + +3. **Changing Bud Geometry:** + + The call for the production of the buds occurs in the ``grow_one(o)`` section by addicting ``spiked_bud(o.thickness)`` to the ``produce`` call: + + .. code-block:: python + + if 'Spur' in o.name: + produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) spiked_bud(o.thickness)grow_object(o) + + The actual bud is produced in the ``spiked_bud`` production section: + + .. code-block:: python + + spiked_bud(r): + base_height = r * 2 + top_height = r * 2 + num_sect = 20 + produce @g(Cylinder(r,base_height,1,num_sect))f(base_height)@g(Cone(r,top_height,0,num_sect)) + + This is one of the most basic objects generated on the tree model. As the buds on actual trees are little spikes, the bud geometry is made up of a cone on top of a cylinder. These are both produced with L-Py's basic ``@g`` primitive used to draw PglShapes. The height of the two shapes scale with the radius (provided as the parameter). The ``num_sect`` is used to determine how many sections each shape is made up of, and 20 was chosen as they appear circular without adding too many triangles to the model. + + + +4. **Changing Branch Profile Curve:** + + Every branch on the model has a unique profile curve. This is so the model doesn't appear to be perfectly cylindrical (as if made of PVC pipes). Every branch has its own unique curve that is generated when the branch is originally generated. This can be found in the actual class declaration at the beginning of the code: + + .. code-block:: python + + self.contour = create_noisy_circle_curve(1, .2, 30) + + Every ``Trunk``, ``Branch``, and ``NonBranch`` object have a their own unique curve associated with them. However this curve is only applied in the ``grow_one(o)`` section: + + .. code-block:: python + + if 'Trunk' in o.name or 'Branch' in o.name: + nproduce SetContour(o.contour) + else: + reset_contour() + + This code targets every ``Trunk``, ``Branch``, and ``NonBranch`` object and sets their own curve when growing them. Whenever any other object is passed to the ``grow_object(o)`` function the call to ``reset_contour()`` sets the contour back to a perfect circle to ensure that no curves overlap. + + The ``create_noisy_circle()`` function is included in the helper.py file. It works by creating a circle out of a given number of points, and then moving those points in the x and y direction according to a given noise factor. The function has two required parameters and two optional parameters. The two required parameters are ``radius`` and ``noise_factor``. The ``radius`` determines the radius of the generated circle. The ``noise_factor`` is used to set a range in which random points will be generated. The points making up the circle will then be moved a random amount in that range. This can be seen in the main ``for`` loop in the function: + + .. code-block:: python + + for angle in t: + # Base circle points + x = radius * cos(angle) + y = radius * sin(angle) + + # Add noise + noise_x = uniform(-noise_factor, noise_factor) + noise_y = uniform(-noise_factor, noise_factor) + + noisy_x = x + noise_x + noisy_y = y + noise_y + + points.append((noisy_x, noisy_y)) + + The two optional parameters for the function are ``num_points`` and ``seed``. ``num_points`` is used to determine how many points make up the circle. If no value is given, it defaults to 100. ``seed`` is used to set the randomness of the circles. If a value is given, the random.seed is set to that value. For this model, seeds were not used. + +5. **Changing Tertiary Branch Curves:** + + Every tertiary branch on the model follows a unique curve. This curve is generated with the ``create_bezier_curve()`` function which can be found in the helper.py file. The function works by generating four points to be used as guide points for the Bézier curve. There is some control code to make sure that the x points are random but are still generated in a linear fashion. + + The function takes four optional parameters: ``num_control_points``, ``x_range``, ``y_range``, and ``seed_val``. ``num_control_points`` defaults to four, the standard for Bézier curves. ``x_range`` and ``y_range`` default to a tuples designating the ranges: (0,10) and (-2,2) respectively. ``seed_val`` defaults to ``None``, however in the actual model ``time.time()`` is used as the seed. -The documentation is provided at https://treesim-lpy.readthedocs.io/en/latest/ -You can find the L-Py documentation at - + The actual designation of the curves for the tertiary branches can be seen in the ``bud(t)`` section: + + .. code-block:: python + + if 'NonTrunk' in new_object.name: + import time + curve = create_bezier_curve(seed_val=time.time()) + nproduce [SetGuide(curve, new_object.max_length) + + A curve is generated with the ``create_bezier_curve()`` and it is used as the guide for the ``SetGuide`` function provided by L-Py. + +6. **Changing color ID system:** + + Every object on the tree has its own unique color code to act as an ID. The implementation of this can be seen in the ``grow_object(o)`` section: + + .. code-block:: python + + r, g, b = o.color + nproduce SetColor(r, g, b) + + smallest_color = [r, g, b].index(min([r, g, b])) + o.color[smallest_color] += 1 + + This works by finding the smallest value of the objects RGB code and incrementing it by one. This is to avoid any kind of error where a color value is greater than 255. To get rid of the color IDs, simply comment out the second two lines of the code above. + + + +7. **Changing Apple and Leaf ratio:** + + The generation of apples and leaves is random. If something is to grow off of the bud, there is a 90% chance it will be a leaf, and a 10% chance it will be an apple. These percentages are set in the ``Spur`` class declaration: + + .. code-block:: python + + # From Spur class + def create_branch(self): + if self.num_leaves < self.max_leaves: + self.num_leaves += 1 + if rd.random()<0.9: + new_ob = Leaf(copy_from = self.prototype_dict['leaf']) + else: + new_ob = Apple(copy_from = self.prototype_dict['apple']) + else: + new_ob = None + + return new_ob + + + + +Features +-------- + +- **Pruning:** Remove unwanted branches to simulate pruning. +- **Branch Tying:** Simulate branches being tied down to mimic different orchard architectures. +- **Model Class Types:** The model generated is built with classes of different material type. ======== Gallery ======== -.. figure:: media/envy.png +.. figure:: media/envy_model.png :width: 500 - :height: 300 + :height: 500 Example of a labelled, pruned and tied envy tree system using TreeSim_Lpy @@ -30,15 +277,13 @@ Gallery :height: 300 Example of a labelled, pruned and tied UFO tree system using TreeSim_Lpy - +Contact +------- -============= -Documentation -============= +For any questions or issues, please contact us through **GitHub Issues**. -Documentation is available at ``_ Help and Support ---------------- diff --git a/examples/Camp_Envy_tie_prune_label.lpy b/examples/Camp_Envy_tie_prune_label.lpy new file mode 100644 index 0000000..1804e6a --- /dev/null +++ b/examples/Camp_Envy_tie_prune_label.lpy @@ -0,0 +1,598 @@ +""" +Tying, Pruning, and Labelling Envy architecture tree +""" +import sys +sys.path.append('../') +from stochastic_tree import Support, BasicWood +import numpy as np +import random as rd +import copy +import gc +import time +import math +from math import sin, cos, pi +from helper import * + + +class Leaf(BasicWood): + count = 0 + def __init__(self, copy_from=None, max_buds_segment=3, thickness=0.05, + thickness_increment=0.005, growth_length=0.5, max_length=2.0, + tie_axis=(0, 1, 1), order=2, prototype_dict={}, name=None, color=None): + super().__init__(copy_from, max_buds_segment, thickness, thickness_increment, growth_length, + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + if not name: + self.name = str(self.__class__.__name__) + '_' + str(self.__class__.count) + Leaf.count += 1 + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.05) + + def create_branch(self): + # Leafs typically do not create further branches, return None + return None + + def grow(self): + #self.num_branches += 1 + #return + pass + +class Apple(BasicWood): + count = 0 + def __init__(self, copy_from=None, max_buds_segment=3, thickness=0.05, + thickness_increment=0.005, growth_length=0.5, max_length=2.0, + tie_axis=(0, 1, 1), order=2, prototype_dict={}, name=None, color=None): + super().__init__(copy_from, max_buds_segment, thickness, thickness_increment, growth_length, + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + if not name: + self.name = str(self.__class__.__name__) + '_' + str(self.__class__.count) + Apple.count += 1 + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.05) + + def create_branch(self): + # Leafs typically do not create further branches, return None + return None + + def grow(self): + #self.num_branches += 1 + #return + pass + +class Spur(BasicWood): + count = 0 + def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ + thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ + tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): + super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + if not name: + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) + Spur.count+=1 + self.num_leaves = 0 + self.contour = create_noisy_circle_curve(1, .2, 30) + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.1) + + def create_branch(self): + if self.num_leaves < 5: + self.num_leaves += 1 + if rd.random()<0.9: + new_ob = Leaf(copy_from = self.prototype_dict['leaf']) + else: + new_ob = Apple(copy_from = self.prototype_dict['apple']) + else: + new_ob = None + return new_ob + + def grow(self): + pass + +class Branch(BasicWood): + count = 0 + def __init__(self, copy_from = None, max_buds_segment: int = 10, thickness: float = 0.1,\ + thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ + tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): + + super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + + if not name: + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) + Branch.count+=1 + self.num_buds_segment=0 + self.contour = create_noisy_circle_curve(1, .2, 30) + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.01*(1 - self.num_buds_segment/self.max_buds_segment)) + + def create_branch(self): + self.num_buds_segment += 1 + if rd.random()>0.6: + new_ob = NonTrunk(copy_from = self.prototype_dict['nontrunk']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + return new_ob + + def grow(self): + pass + +class Trunk(BasicWood): + count = 0 + """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ + def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ + thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ + tie_axis: tuple = (0,1,1), order: int = 0, prototype_dict: dict = {}, name = None, color = None): + + super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + if not name: + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) + Trunk.count+=1 + self.contour = create_noisy_circle_curve(1, .2, 30) + + def is_bud_break(self, num_buds_segment): + if (rd.random() > 0.2*(1 - num_buds_segment/self.max_buds_segment)): + return False + return True + + def create_branch(self): + if rd.random() > 0.7: + return Spur(copy_from = self.prototype_dict['spur']) + else: + return Branch(copy_from = self.prototype_dict['branch']) + + def grow(self): + pass + +class NonTrunk(BasicWood): + count = 0 + def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ + thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ + tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): + + super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + + if not name: + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) + Branch.count+=1 + self.contour = create_noisy_circle_curve(1, .2, 30) + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) + + def create_branch(self): + if rd.random()>0.9: + return None + #new_ob = Spur(copy_from = self.prototype_dict['nontrunkbranch']) + else: + new_ob = Spur(copy_from = self.prototype_dict['spur']) + return new_ob + + def grow(self): + pass + +#Pass transition probabs? --> solve with abstract classes +growth_length = 0.1 +#everything is relative to growth length +basicwood_prototypes = {} +basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 0.4/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.0002, prototype_dict = basicwood_prototypes, color = [255,0,0] ) +basicwood_prototypes['branch'] = Branch(tie_axis = (0,1,1), max_length = .22/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) +basicwood_prototypes['nontrunk'] = NonTrunk(tie_axis = (0,1,1), max_length = 0.05/growth_length, growth_length = 0.1, thickness = 0.003,thickness_increment = 0.0001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) +basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.0008/growth_length, thickness = 0.003, growth_length = 0.2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0,255,0] ) + +#---------------------- +# TESTING - CAMP +basicwood_prototypes['leaf'] = Leaf(tie_axis=(0, 1, 1), max_length=0.02/growth_length, thickness=0.01, growth_length=0.05, thickness_increment=0.0005, prototype_dict=basicwood_prototypes, color=[36, 117, 32]) + +basicwood_prototypes['apple'] = Apple(tie_axis=(0, 1, 1), max_length=0.02/growth_length, thickness=0.01, growth_length=0.05, thickness_increment=0.0005, prototype_dict=basicwood_prototypes, color=[36, 117, 32]) +#---------------------- + +#init +trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) +time_count = 0 +def generate_points_v_trellis(): + x = np.full((7,), 0.45).astype(float) + #z = np.arange(3, 24, 3).astype(float) + y = np.full((7,), 0).astype(float) + z = np.arange(0.6,3.4, 0.45) + #print(z) + pts = [] + id = 0 + for i in range(x.shape[0]): + pts.append((-x[i], y[i], z[i])) + id+=1 + pts.append((x[i], y[i], z[i])) + id+=1 + return pts +# points, num_wires spacing wires trunk_wire_pt branch_axis trunk_axis +support = Support(generate_points_v_trellis(), 14 , 1 , None, (0,0,1), None) +num_iteration_tie = 20 +###Tying stuff begins + +def ed(a,b): + return (a[0]-b[0])**2+(a[1]-b[1])**2+(a[2]-b[2])**2 + +def get_energy_mat(branches, arch): + num_branches = len(branches) + num_wires = len(list(arch.branch_supports.values())) + energy_matrix = np.ones((num_branches,num_wires))*np.inf + #print(energy_matrix.shape) + for branch_id, branch in enumerate(branches): + if branch.has_tied: + continue + for wire_id, wire in arch.branch_supports.items(): + if wire.num_branch>=1: + continue + energy_matrix[branch_id][wire_id] = ed(wire.point,branch.end)/2+ed(wire.point,branch.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) + + return energy_matrix + +def decide_guide(energy_matrix, branches, arch): + for i in range(energy_matrix.shape[0]): + min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) + #print(min_arg) + if(energy_matrix[min_arg[0][0]][min_arg[0][1]] == np.inf) or energy_matrix[min_arg[0][0]][min_arg[0][1]] > 1: + return + if not (branches[min_arg[0][0]].has_tied == True):# and not (arch.branch_supports[min_arg[0][1]].num_branch >=1): + #print("Imp:",min_arg[0][0], min_arg[0][1], energy_matrix[min_arg[0][0]][min_arg[0][1]]) + branches[min_arg[0][0]].guide_target = arch.branch_supports[min_arg[0][1]]#copy.deepcopy(arch.branch_supports[min_arg[0][1]].point) + #trellis_wires.trellis_pts[min_arg[0][1]].num_branches+=1 + for j in range(energy_matrix.shape[1]): + energy_matrix[min_arg[0][0]][j] = np.inf + for j in range(energy_matrix.shape[0]): + energy_matrix[j][min_arg[0][1]] = np.inf + +def tie(lstring): + for j,i in enumerate(lstring): + if i == 'C' and i[0].type.__class__.__name__ == 'Branch': + if i[0].type.tie_updated == False: + continue + curr = i[0] + if i[0].type.guide_points: + #print("tying ", i[0].type.name, i[0].type.guide_target.point) + i[0].type.tie_updated = False + i[0].type.guide_target.add_branch() + lstring, count = i[0].type.tie_lstring(lstring, j) + + return True + return False + +#Pruning strategy +def pruning_strategy(lstring): #Remove remnants of cut + cut = False + for j,i in enumerate(lstring): + if i.name == 'C' and i[0].type.age > 6 and i[0].type.has_tied == False and i[0].type.cut == False: + + i[0].type.cut = True + #print("Cutting", i[0].type.name) + lstring = cut_using_string_manipulation(j, lstring) + return True + return False + +def StartEach(lstring): + global parent_child_dict + for i in parent_child_dict[trunk_base.name]: + if i.tie_updated == False: + i.tie_update() + + +def EndEach(lstring): + global parent_child_dict, support + tied = False + #print(getIterationNb()) + if (getIterationNb()+1)%num_iteration_tie == 0: + energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) + #print(energy_matrix) + decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) + for branch in parent_child_dict[trunk_base.name]: + branch.update_guide(branch.guide_target) + #print(branch.name, branch.guide_target) + while tie(lstring): + pass + while pruning_strategy(lstring): + pass + return lstring + +parent_child_dict = {} +parent_child_dict[trunk_base.name] = [] +#print(generate_points_ufo()) +module Attractors +module grow_object +module bud +module branch +module C +extern(label = True) +extern(seed_val = 0) +rd.seed(seed_val) + + +# Added by Camp +def create_noisy_circle_curve(radius, noise_factor, num_points=100, seed=None): + if seed is not None: + rd.seed(seed) + t = np.linspace(0, 2 * np.pi, num_points, endpoint=False) + points = [] + for angle in t: + # Base circle points + x = radius * np.cos(angle) + y = radius * np.sin(angle) + + # Add noise + noise_x = rd.uniform(-noise_factor, noise_factor) + noise_y = rd.uniform(-noise_factor, noise_factor) + + noisy_x = x + noise_x + noisy_y = y + noise_y + + points.append((noisy_x, noisy_y)) + + # Ensure the curve is closed by adding the first point at the end + points.append(points[0]) + + # Create the PlantGL Point2Array and Polyline2D + curve_points = Point2Array(points) + curve = Polyline2D(curve_points) + return curve + + +def create_bezier_curve(num_control_points=4, x_range=(0, 10), y_range=(-2, 2), seed=None): + if seed is not None: + rd.seed(seed) # Set the random seed for reproducibility + # Generate progressive control points within the specified ranges + control_points = [] + prev_x = rd.uniform(x_range[0], x_range[1] / 4) + for i in range(num_control_points): + x = prev_x + rd.uniform(0, (x_range[1] - prev_x) / (num_control_points - i)) + y = rd.uniform(*y_range) + control_points.append(Vector3(x, y, 0)) # Set z to 0 for 2D curve + prev_x = x + # Create a Point3Array from the control points + control_points_array = Point3Array(control_points) + # Create and return the BezierCurve2D object + bezier_curve = BezierCurve2D(control_points_array) + return bezier_curve + +def rgb_seed(r, g, b): + return r * 256**2 + g * 256 + b + +def make_apple_curve(): + y1 = rd.uniform(.65,1) + y2 = rd.uniform(.8,1) + y3 = rd.uniform(.4,.5) + + # points from apple curve + # 0: (0, 0, 1) + # 1: (0, 1, 1) + # 2: (1, 0.807331, 1) + # 3: (1, 0.461907, 1) + #y3 = 0.439871 + + base_points = [ + #point 0 #point 1 #point 2 #point 3 + (0, 0, 1), (0, y1, 1), (1, y2, 1),(1, y3, 1) + ] + + top_points = [ + (0, y3, 1),(0, 0.118695, 1),(0.909233, 0.146624, 1),(1, -0.0977491, 1) + ] + + base_point_list = Point3Array(base_points) + curve1 = NurbsCurve2D(base_point_list) + base_curve = QuantisedFunction(curve1) + + top_point_list = Point3Array(top_points) + curve2 = BezierCurve2D(top_point_list) + top_curve = QuantisedFunction(curve2) + + return [base_curve,top_curve] + +def make_stem_curve(): + + # points from stem curve + # 0: (-1.48289, 0.982887, 1) + # 1: (-0.559816, 1.22861, 1.05),(1.05126, 1.57262, 1),(1.70403, -0.0245722, 1) + # 2: (1.05126, 1.57262, 1) + # 3: (1.70403, -0.0245722, 1) + + points = [ + (-1.48289, 0.982887, 1),(-0.559816, 1.22861, 1.05),(1.05126, 1.57262, 1),(1.70403, -0.0245722, 1) + ] + ctrlPointList = Point3Array(points) + stem_curve = NurbsCurve2D(ctrlPointList) + return stem_curve + +base_height = 2.5 +rad = 2 + +def make_leaf_guide(): + + x1 = rd.uniform(0,.25) + x2 = rd.uniform(.65,.85) + y1 = rd.uniform(.45,.55) + y2 = rd.uniform(.25,.35) + + control_points = [ + Vector3(0,0,0), + Vector3(x1,y1,0), + Vector3(x2,y2,0), + Vector3(1,0,0), + ] + + inverse_points = [ + Vector3(0,0,0), + Vector3(x1,-y1,0), + Vector3(x2,-y2,0), + Vector3(1,0,0), + ] + + side1 = Point3Array(control_points) + side2 = Point3Array(inverse_points) + + curve1 = BezierCurve2D(side1) + curve2 = BezierCurve2D(side2) + + return [curve1, curve2] + + +def reset_contour(): + default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour + nproduce SetContour(default_curve) + +global profile1, profile2, profile3 +profile1 = create_noisy_circle_curve(1, .2, 30, 23) +profile2 = create_noisy_circle_curve(1, .2, 30) +profile3 = create_noisy_circle_curve(1, .2, 30) +# print("Labelling: ", label) +# print("Seed ", seed_val) + + +Axiom: Attractors(support)grow_object(trunk_base) +derivation length: 105 +production: +#Decide whether branch internode vs trunk internode need to be the same size. +grow_object(o) : + #global profile1 + if o == None: + produce * + else: + if o.length >= o.max_length: + o.age += 1 + nproduce * + else: + # Apply color + r, g, b = o.color + nproduce SetColor(r, g, b) + # set unique color ID + smallest_color = [r, g, b].index(min([r, g, b])) + o.color[smallest_color] += 1 + + # Check if it's a trunk or branch, then apply the contour + if 'Trunk' in o.name or 'Branch' in o.name: + #print("TRUNK BRANCH NAME: " + o.name) + radius = o.thickness + noise_factor = 0.8 + num_points = 60 + nproduce SetContour(o.contour) + #pass + else: + reset_contour() + + o.grow_one() + if 'Spur' in o.name: + produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) spiked_bud(o.thickness)grow_object(o) + elif 'Leaf' in o.name: + produce L(.1) + elif 'Apple' in o.name: + #pass + #print("APPLE NAME: " + o.name) + #produce [S(.1, .007)Ap] + produce [S(.1/2, .09/15)f(.1)&(180)A(.1, .09)] + else: + produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) grow_object(o) +#L(.1) + +bud(t) : + if t.type.is_bud_break(t.num_buds): + new_object = t.type.create_branch() + if new_object == None: + produce * + parent_child_dict[new_object.name] = [] + parent_child_dict[t.type.name].append(new_object) + #Store new object somewhere + t.num_buds+=1 + t.type.num_branches+=1 + + # set a different cross section for every branch + if 'Leaf' not in new_object.name and 'Apple' not in new_object.name: + #print("OBJECT NAME: " + new_object.name) + nproduce SetContour( new_object.contour ) + + # set a curve for tertiary branches to follow as they grow + if 'NonTrunk' in new_object.name: + import time + r, g, b = new_object.color + seed = rgb_seed(r, g, b) + curve = create_bezier_curve(seed=time.time()) + nproduce [SetGuide(curve, new_object.max_length) + else: + nproduce [ + nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.end)]bud(t) + +spiked_bud(r): + base_height = r * 2 + top_height = r * 2 + num_sect = 20 + produce @g(Cylinder(r,base_height,1,num_sect))f(base_height)@g(Cone(r,top_height,0,num_sect)) + +A(bh, r): + curves = make_apple_curve() + base_curve = curves[0] + top_curve = curves[1] + nproduce SetColor(230,0,0) SectionResolution(60) + produce nF(bh, .01, r, base_curve) ^(180) nF(bh/5, .1, r, top_curve)^(180)#S(bh/2,r/15) + +S(sh,r): + stem_curve = make_stem_curve() + nproduce SetColor(100,65,23) + produce SetGuide(stem_curve, sh) _(r)nF(sh, .1, r) #Ap + + +Ap: + nproduce SetColor(255,0,0)SectionResolution(60) + produce @O(rd.uniform(.035, .05) ) + +L(l): + nproduce SetColor(0,225,0) + # Setting the color to magenta to better see the leaves + #nproduce SetColor(255,0,255) + + curves = make_leaf_guide() + curve1 = curves[0] + curve2 = curves[1] + + #produce ~l(l) + + # This is the line that should be drawing the leaf + # however only the first "F" actually shows up. The {} section which is the actual leaf never appears + produce _(.0025) F(l/10){[SetGuide(curve1, l) _(.001).nF(l, .01)][SetGuide(curve2, l)_(.001).nF(l, .01)]} + + +I(s,r,o) --> I(s,r+o.thickness_increment, o) +#_(r) --> _(r+o.thickness_increment) +_(r) --> _(r) +homomorphism: +I(a,r,o) --> F(a,r) +S(a,r,o) --> F(a,r) +production: +Attractors(support): + pttodisplay = support.attractor_grid.get_enabled_points() + if len(pttodisplay) > 0: + produce [,(3) @g(PointSet(pttodisplay,width=10))] diff --git a/examples/Envy_tie_prune_label.lpy b/examples/Envy_tie_prune_label.lpy index 22207cf..becea5d 100644 --- a/examples/Envy_tie_prune_label.lpy +++ b/examples/Envy_tie_prune_label.lpy @@ -9,65 +9,142 @@ import random as rd import copy import gc import time - +import math +from math import sin, cos, pi from helper import * -#context().turtle.setMaterial(9,ImageTexture('../light-tree-bark-bl/light-tree-bark_albedo.png')) -#context().turtle.setMaterial(10,ImageTexture('../white-spruce-tree-bark-bl/white-spruce-tree-bark-albedo.png')) -material = {"Spur": 9, "Branch": 10, "Trunk": 10, "NonTrunk": 9} + +# Leaf class +# Built on the BasicWood class in order to retain the same functionality as the other +# parts of the tree. +class Leaf(BasicWood): + count = 0 + def __init__(self, copy_from=None, max_buds_segment=3, thickness=0.05, + thickness_increment=0.005, growth_length=0.5, max_length=2.0, + tie_axis=(0, 1, 1), order=2, prototype_dict={}, name=None, color=None): + super().__init__(copy_from, max_buds_segment, thickness, thickness_increment, growth_length, + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + if not name: + self.name = str(self.__class__.__name__) + '_' + str(self.__class__.count) + Leaf.count += 1 + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.05) + + # Useless but necesessay as this function gets called in the bud(t) section + def create_branch(self): + # Leafs typically do not create further branches + return None + + def grow(self): + pass + + +# Apple class +# Built on the BasicWood class in order to retain the same functionality as the other +# parts of the tree. +class Apple(BasicWood): + count = 0 + def __init__(self, copy_from=None, max_buds_segment=3, thickness=0.05, + thickness_increment=0.005, growth_length=0.5, max_length=2.0, + tie_axis=(0, 1, 1), order=2, prototype_dict={}, name=None, color=None): + super().__init__(copy_from, max_buds_segment, thickness, thickness_increment, growth_length, + max_length, tie_axis, order, color) + if copy_from: + self.__copy_constructor__(copy_from) + else: + self.prototype_dict = prototype_dict + if not name: + self.name = str(self.__class__.__name__) + '_' + str(self.__class__.count) + Apple.count += 1 + + def is_bud_break(self, num_buds_segment): + return (rd.random() < 0.05) + + # Useless but necesessay as this function gets called in the bud(t) section + def create_branch(self): + # Apples typically do not create further branches + return None + + def grow(self): + pass + class Spur(BasicWood): count = 0 def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None, material = None): + tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None,\ + max_leaves: int = 5): super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color, material) + max_length, tie_axis, order, color) if copy_from: self.__copy_constructor__(copy_from) else: self.prototype_dict = prototype_dict if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) + Spur.count+=1 + # Maximum number of things to grow off of a bud + self.max_leaves = max_leaves + # Keep count of number of leaves to not go over max_leaves + self.num_leaves = 0 - Spur.count+=1 + # Every branch gets its own contour when it is constructed to ensure + # branches each having a unique profile curve + self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): return (rd.random() < 0.1) - + + # Generates a leaf or apples def create_branch(self): - return None + if self.num_leaves < self.max_leaves: + self.num_leaves += 1 + if rd.random()<0.9: + new_ob = Leaf(copy_from = self.prototype_dict['leaf']) + else: + new_ob = Apple(copy_from = self.prototype_dict['apple']) + else: + new_ob = None + return new_ob def grow(self): pass class Branch(BasicWood): count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 2, thickness: float = 0.1,\ + def __init__(self, copy_from = None, max_buds_segment: int = 10, thickness: float = 0.1,\ thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None, material = None): + tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color, material) + max_length, tie_axis, order, color) if copy_from: self.__copy_constructor__(copy_from) else: self.prototype_dict = prototype_dict if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 self.num_buds_segment=0 + + # Every branch gets its own contour when it is constructed to ensure + # branches each having a unique profile curve + self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - - return (rd.random() < 0.1*self.growth_length*(1 - num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.01*(1 - self.num_buds_segment/self.max_buds_segment)) def create_branch(self): self.num_buds_segment += 1 - if rd.random()<0.2: + if rd.random()>0.6: new_ob = NonTrunk(copy_from = self.prototype_dict['nontrunk']) else: new_ob = Spur(copy_from = self.prototype_dict['spur']) @@ -79,29 +156,31 @@ class Branch(BasicWood): class Trunk(BasicWood): count = 0 """ Details of the trunk while growing a tree, length, thickness, where to attach them etc """ - def __init__(self, copy_from = None, max_buds_segment: int = 3, thickness: float = 0.1,\ + def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 0, prototype_dict: dict = {}, name = None, color = None, material = None): + tie_axis: tuple = (0,1,1), order: int = 0, prototype_dict: dict = {}, name = None, color = None): super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color, material) + max_length, tie_axis, order, color) if copy_from: self.__copy_constructor__(copy_from) else: self.prototype_dict = prototype_dict if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - print(material) - + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Trunk.count+=1 + # Every branch gets its own contour when it is constructed to ensure + # branches each having a unique profile curve + self.contour = create_noisy_circle_curve(1, .2, 30) + def is_bud_break(self, num_buds_segment): - if (rd.random() > 0.1*self.growth_length*(1 - num_buds_segment/self.max_buds_segment)): + if (rd.random() > 0.2*(1 - num_buds_segment/self.max_buds_segment)): return False return True def create_branch(self): - if rd.random() > 0.9: + if rd.random() > 0.7: return Spur(copy_from = self.prototype_dict['spur']) else: return Branch(copy_from = self.prototype_dict['branch']) @@ -109,27 +188,29 @@ class Trunk(BasicWood): def grow(self): pass - class NonTrunk(BasicWood): count = 0 - def __init__(self, copy_from = None, max_buds_segment: int = 2, thickness: float = 0.1,\ + def __init__(self, copy_from = None, max_buds_segment: int = 5, thickness: float = 0.1,\ thickness_increment: float = 0.01, growth_length: float = 1., max_length: float = 7.,\ - tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None, material = None): + tie_axis: tuple = (0,1,1), order: int = 1, prototype_dict: dict = {}, name = None, color = None): super().__init__(copy_from, max_buds_segment,thickness, thickness_increment, growth_length,\ - max_length, tie_axis, order, color, material) + max_length, tie_axis, order, color) if copy_from: self.__copy_constructor__(copy_from) else: self.prototype_dict = prototype_dict if not name: - self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) - + self.name = str(self.__class__.__name__) +'_'+ str(self.__class__.count) Branch.count+=1 + + # Every branch gets its own contour when it is constructed to ensure + # branches each having a unique profile curve + self.contour = create_noisy_circle_curve(1, .2, 30) def is_bud_break(self, num_buds_segment): - return (rd.random() < 0.05*self.growth_length*(1 - num_buds_segment/self.max_buds_segment)) + return (rd.random() < 0.02*(1 - num_buds_segment/self.max_buds_segment)) def create_branch(self): if rd.random()>0.9: @@ -142,25 +223,30 @@ class NonTrunk(BasicWood): def grow(self): pass -# Pass transition probabs? --> solve with abstract classes -growth_length = 0.04 +#Pass transition probabs? --> solve with abstract classes +growth_length = 0.1 #everything is relative to growth length basicwood_prototypes = {} -basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 4, thickness = 0.025, growth_length = growth_length,thickness_increment = 0.0002*growth_length, prototype_dict = basicwood_prototypes, color = 0, material = None) -basicwood_prototypes['branch'] = Branch(tie_axis = (0,1,1), max_length = 2.2, thickness = 0.01, growth_length = growth_length,thickness_increment = 0.00001*growth_length, prototype_dict = basicwood_prototypes, color = 1, material = None) -basicwood_prototypes['nontrunk'] = NonTrunk(tie_axis = (0,1,1), max_length = 0.2, growth_length = growth_length, thickness = 0.003,thickness_increment = 0.0001*growth_length, prototype_dict = basicwood_prototypes, color = 3, material = None) -basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.08, thickness = 0.003, growth_length = growth_length, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = 2, material = None) +basicwood_prototypes['trunk'] = Trunk(tie_axis = (0,1,1), max_length = 0.4/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.0002, prototype_dict = basicwood_prototypes, color = [255,0,0] ) +basicwood_prototypes['branch'] = Branch(tie_axis = (0,1,1), max_length = .22/growth_length, thickness = 0.01, growth_length = 0.1,thickness_increment = 0.00001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) +basicwood_prototypes['nontrunk'] = NonTrunk(tie_axis = (0,1,1), max_length = 0.05/growth_length, growth_length = 0.1, thickness = 0.003,thickness_increment = 0.0001, prototype_dict = basicwood_prototypes, color = [255,150,0] ) +basicwood_prototypes['spur'] = Spur(tie_axis = (0,1,1), max_length = 0.0008/growth_length, thickness = 0.003, growth_length = 0.2, thickness_increment = 0., prototype_dict = basicwood_prototypes, color = [0,255,0], max_leaves = 5 ) + +# Declare leaf and apple in a similar fashion. +basicwood_prototypes['leaf'] = Leaf(tie_axis=(0, 1, 1), max_length=0.02/growth_length, thickness=0.01, growth_length=0.05, thickness_increment=0.0005, prototype_dict=basicwood_prototypes, color=[36, 117, 32]) + +basicwood_prototypes['apple'] = Apple(tie_axis=(0, 1, 1), max_length=0.02/growth_length, thickness=0.01, growth_length=0.05, thickness_increment=0.0005, prototype_dict=basicwood_prototypes, color=[36, 117, 32]) + #init trunk_base = Trunk(copy_from = basicwood_prototypes['trunk']) time_count = 0 - def generate_points_v_trellis(): x = np.full((7,), 0.45).astype(float) #z = np.arange(3, 24, 3).astype(float) y = np.full((7,), 0).astype(float) z = np.arange(0.6,3.4, 0.45) - print(z) + #print(z) pts = [] id = 0 for i in range(x.shape[0]): @@ -169,13 +255,9 @@ def generate_points_v_trellis(): pts.append((x[i], y[i], z[i])) id+=1 return pts - - - -support = Support(generate_points_v_trellis(), 14 , 1 , None, (0,0,1), None) -num_iteration_tie = 30 -num_iteration_first_tie = 60 -first_tied = False +# points, num_wires spacing wires trunk_wire_pt branch_axis trunk_axis +support = Support(generate_points_v_trellis(), 14 , 1 , None, (0,0,1), None) +num_iteration_tie = 20 ###Tying stuff begins def ed(a,b): @@ -192,9 +274,10 @@ def get_energy_mat(branches, arch): for wire_id, wire in arch.branch_supports.items(): if wire.num_branch>=1: continue - energy_matrix[branch_id][wire_id] = np.square(ed(wire.point,branch.start))/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age)ed(wire.point,branch.end)/2+ - return energy_matrix + energy_matrix[branch_id][wire_id] = ed(wire.point,branch.end)/2+ed(wire.point,branch.start)/2#+v.num_branches*10+branch.bend_energy(deflection, curr_branch.age) + return energy_matrix + def decide_guide(energy_matrix, branches, arch): for i in range(energy_matrix.shape[0]): min_arg = np.argwhere(energy_matrix == np.min(energy_matrix)) @@ -209,7 +292,7 @@ def decide_guide(energy_matrix, branches, arch): energy_matrix[min_arg[0][0]][j] = np.inf for j in range(energy_matrix.shape[0]): energy_matrix[j][min_arg[0][1]] = np.inf - + def tie(lstring): for j,i in enumerate(lstring): if i == 'C' and i[0].type.__class__.__name__ == 'Branch': @@ -226,14 +309,13 @@ def tie(lstring): return False #Pruning strategy - def pruning_strategy(lstring): #Remove remnants of cut cut = False for j,i in enumerate(lstring): if i.name == 'C' and i[0].type.age > 6 and i[0].type.has_tied == False and i[0].type.cut == False: i[0].type.cut = True - print("Cutting", i[0].type.name) + #print("Cutting", i[0].type.name) lstring = cut_using_string_manipulation(j, lstring) return True return False @@ -241,16 +323,15 @@ def pruning_strategy(lstring): #Remove remnants of cut def StartEach(lstring): global parent_child_dict for i in parent_child_dict[trunk_base.name]: - if i.tie_updated == False: - i.tie_update() + if i.tie_updated == False: + i.tie_update() def EndEach(lstring): - global parent_child_dict, support, first_tied, num_iteration_first_tie + global parent_child_dict, support tied = False - print(getIterationNb()) - if (getIterationNb()+1)%num_iteration_tie == 0 and (first_tied or (getIterationNb()+1)%num_iteration_first_tie == 0): - first_tied = True + #print(getIterationNb()) + if (getIterationNb()+1)%num_iteration_tie == 0: energy_matrix = get_energy_mat(parent_child_dict[trunk_base.name], support) #print(energy_matrix) decide_guide(energy_matrix, parent_child_dict[trunk_base.name], support) @@ -264,84 +345,234 @@ def EndEach(lstring): return lstring parent_child_dict = {} -parent_child_dict[trunk_base.name] = [] +parent_child_dict[trunk_base.name] = [] #print(generate_points_ufo()) module Attractors module grow_object module bud module branch module C -extern(label = False) +extern(label = True) extern(seed_val = 0) rd.seed(seed_val) -# print("Labelling: ", label) -# print("Seed ", seed_val) -Axiom: Attractors(support)grow_object(trunk_base) -derivation length: 350 +# When called, produce two curves to guide the production of an apple, +# returns the two curves together in an array +def make_apple_curve(): + + # Generate random points in a small range to produce apples of varying size + y1 = rd.uniform(.65,1) + y2 = rd.uniform(.8,1) + y3 = rd.uniform(.4,.5) + + # Points used to build the curve that makes up the base of the apple + base_points = [ + #point 0 #point 1 #point 2 #point 3 + (0, 0, 1), (0, y1, 1), (1, y2, 1),(1, y3, 1) + ] + + # Points used to build the curve that makes up the indentation on the top of the apple + top_points = [ + (0, y3, 1),(0, 0.118695, 1),(0.909233, 0.146624, 1),(1, -0.0977491, 1) + ] + + # To make a valid curve for the nF radiusvariation, + # make a Point3Array of guide points, + # then make a NurbsCurve2D with that array as its parameter, + # then make a QuantisedFunction with the NurbsCurve object as the parameter + base_point_list = Point3Array(base_points) + curve1 = NurbsCurve2D(base_point_list) + base_curve = QuantisedFunction(curve1) + + # The same process can be done with a BezierCurve2D object instead of the NurbsCurve2D + top_point_list = Point3Array(top_points) + curve2 = BezierCurve2D(top_point_list) + top_curve = QuantisedFunction(curve2) + + return [base_curve,top_curve] + + +# Makes a single curve to guide the production of a stem for the apples +def make_stem_curve(): + + # The process of making this curve is the exact same as the apple curves, + # however the same curve is produced every time for the stems. + # Note: turning the NurbsCurve2D into a QuantisedFunction isn't necessary since + # the curve is being used with the SetGuide function + points = [ + (-1.48289, 0.982887, 1),(-0.559816, 1.22861, 1.05),(1.05126, 1.57262, 1),(1.70403, -0.0245722, 1) + ] + ctrlPointList = Point3Array(points) + stem_curve = NurbsCurve2D(ctrlPointList) + return stem_curve + + +# Makes a curve and its inverse curve to guide the two sides of a leaf. +# Also returns both curves together in an array. +def make_leaf_guide(): + + # Guide points for curve, randomly generated to produce leaves of varying size + x1 = rd.uniform(0,.25) + x2 = rd.uniform(.65,.85) + y1 = rd.uniform(.45,.55) + y2 = rd.uniform(.25,.35) + + # Points are Vector3 objects instead of just tuples + control_points = [ + Vector3(0,0,0), + Vector3(x1,y1,0), + Vector3(x2,y2,0), + Vector3(1,0,0), + ] + + inverse_points = [ + Vector3(0,0,0), + Vector3(x1,-y1,0), + Vector3(x2,-y2,0), + Vector3(1,0,0), + ] + + # Make curves from control points + side1 = Point3Array(control_points) + side2 = Point3Array(inverse_points) + + # Make actual BezierCurve2D objects in to be used in SetGuide + curve1 = BezierCurve2D(side1) + curve2 = BezierCurve2D(side2) + + return [curve1, curve2] + +# Used to set the contour back to a circle after it has been changed for a branch +def reset_contour(): + default_curve = create_noisy_circle_curve(1, 0, 30) # Example of a simple default contour + nproduce SetContour(default_curve) + + +Axiom: Attractors(support)grow_object(trunk_base) +derivation length: 105 production: #Decide whether branch internode vs trunk internode need to be the same size. grow_object(o) : if o == None: produce * - if o.length >= o.max_length: - o.age+=1 - nproduce * else: - if label: - nproduce SetColor(o.color) - elif o.material: - nproduce SetColor(o.material) - print(o.material, "material") - o.grow_one() - if 'Spur' in o.name: - produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))@O(0.002)grow_object(o) + if o.length >= o.max_length: + o.age += 1 + nproduce * else: - produce I(o.growth_length, o.thickness, o)bud(ParameterSet(type = o, num_buds = 0))grow_object(o) + # Get object's usual color and apply it + r, g, b = o.color + nproduce SetColor(r, g, b) + + # This sets unique color IDs + # In order to have the tree semantically segmented, + # simply comment out the two following lines + smallest_color = [r, g, b].index(min([r, g, b])) + o.color[smallest_color] += 1 + + # Check if it's a trunk or branch, then apply the contour + if 'Trunk' in o.name or 'Branch' in o.name: + nproduce SetContour(o.contour) + else: + # set the contour back to a usual circle + reset_contour() + + o.grow_one() + + if 'Spur' in o.name: + # note that the production of the buds is here with 'spiked_bud(o.thickness)' + produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) spiked_bud(o.thickness)grow_object(o) + elif 'Leaf' in o.name: + produce L(.1) + elif 'Apple' in o.name: + produce [S(.1/2, .09/15)f(.1)&(180)A(.1, .09)] + else: + # If o is a Trunk, Branch, or NonTrunk, simply produce the internodes + produce I(o.growth_length, o.thickness, o) bud(ParameterSet(type=o, num_buds=0)) grow_object(o) + bud(t) : - if t.type.is_bud_break(t.num_buds): - new_object = t.type.create_branch() - if new_object == None: - produce * - parent_child_dict[new_object.name] = [] - parent_child_dict[t.type.name].append(new_object) - #Store new object somewhere - t.num_buds+=1 - t.type.num_branches+=1 - nproduce [@RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*360)grow_object(new_object)GetPos(new_object.end)]bud(t) - + if t.type.is_bud_break(t.num_buds): + new_object = t.type.create_branch() + if new_object == None: + produce * + + + parent_child_dict[new_object.name] = [] + parent_child_dict[t.type.name].append(new_object) + #Store new object somewhere + t.num_buds+=1 + t.type.num_branches+=1 + + # Produce magenta spheres at every junction + if 'Leaf' not in new_object.name and 'Apple' not in new_object.name: + if 'Non' in new_object.name: + max_thickness = new_object.thickness + ( (200 - getIterationNb()) * new_object.thickness_increment) + else: + max_thickness = t.type.thickness + ( (200 - getIterationNb()) * t.type.thickness_increment) + + nproduce SetColor(255,0,255) @O(max_thickness) -I(s,r,o) --> I(s,r+o.thickness_increment, o) -_(r) --> _(r+o.thickness_increment) + + # Set a curve for tertiary branches to follow as they grow + if 'NonTrunk' in new_object.name: + import time + curve = create_bezier_curve(seed_val=time.time()) + nproduce [SetGuide(curve, new_object.max_length) + else: + nproduce [ + nproduce @RGetPos(new_object.start)C(ParameterSet(type = new_object))/(rd.random()*360)&(rd.random()*90)grow_object(new_object)GetPos(new_object.end)]bud(t) + + +# Simple set of productions to build apple bud. This bud is +# made up of a cylinder and a cone +spiked_bud(r): + base_height = r * 2 + top_height = r * 2 + num_sect = 20 + produce @g(Cylinder(r,base_height,1,num_sect))f(base_height)@g(Cone(r,top_height,0,num_sect)) + + +# Production rules to make apples. The curves are generated, the base +# of the apple is generated, then the turtle turns 180 degrees and +# generates the top indentation of the apple. Stem is created in the S production section +A(bh, r): + curves = make_apple_curve() + base_curve = curves[0] + top_curve = curves[1] + nproduce SetColor(230,0,0) SectionResolution(60) + produce nF(bh, .01, r, base_curve) ^(180) nF(bh/5, .1, r, top_curve)^(180)#S(bh/2,r/15) -homomorphism: +# Productions rules for stem generation. The curve generated from +# make_stem_curve() is used as a guide. +S(sh,r): + stem_curve = make_stem_curve() + nproduce SetColor(100,65,23) + produce SetGuide(stem_curve, sh) _(r)nF(sh, .1, r) #Ap + + +# Production rules for generating leaves. curve1 and curve2 are inverses of each other. +L(l): + nproduce SetColor(0,225,0) + + curves = make_leaf_guide() + curve1 = curves[0] + curve2 = curves[1] + + produce _(.0025) F(l/10){[SetGuide(curve1, l) _(.001).nF(l, .01)][SetGuide(curve2, l)_(.001).nF(l, .01)]} + + +I(s,r,o) --> I(s,r+o.thickness_increment, o) +#_(r) --> _(r+o.thickness_increment) +_(r) --> _(r) +homomorphism: I(a,r,o) --> F(a,r) S(a,r,o) --> F(a,r) - production: Attractors(support): pttodisplay = support.attractor_grid.get_enabled_points() if len(pttodisplay) > 0: - pass - #produce [,(3) @g(PointSet(pttodisplay,width=10))] -###### INITIALISATION ###### - -__lpy_code_version__ = 1.1 - -def __initialiseContext__(context): - import openalea.plantgl.all as pgl - PGL_105553169227360 = pgl.ImageTexture("PGL_105553169227360" , "../light-tree-bark-bl/light-tree-bark_albedo.png" , ) - PGL_105553169227360.name = "PGL_105553169227360" - Color_9 = pgl.Texture2D(image = PGL_105553169227360 , ) - Color_9.name = "Color_9" - context.turtle.setMaterial(9,Color_9) - PGL_105553169228320 = pgl.ImageTexture("PGL_105553169228320" , "../white-spruce-tree-bark-bl/white-spruce-tree-bark-albedo.png" , ) - PGL_105553169228320.name = "PGL_105553169228320" - Color_10 = pgl.Texture2D(image = PGL_105553169228320 , ) - Color_10.name = "Color_10" - context.turtle.setMaterial(10,Color_10) - context.animation_timestep = 0.15 + produce [,(3) @g(PointSet(pttodisplay,width=10))] diff --git a/helper.py b/helper.py index 363ca52..df5eb4c 100644 --- a/helper.py +++ b/helper.py @@ -1,13 +1,15 @@ -from openalea.plantgl.all import NurbsCurve +from openalea.plantgl.all import NurbsCurve, Vector3, Point2Array, Point3Array, Polyline2D, BezierCurve2D from openalea.lpy import Lsystem, newmodule -from random import uniform +from random import uniform, seed +from numpy import linspace, pi, sin, cos + def amplitude(x): return 2 def cut_from(pruning_id, s, path = None): """Check cut_string_from_manipulation for manual implementation""" - #s.insertAt(pruning_id, newmodule('F')) - s.insertAt(pruning_id, newmodule('%')) + s.insertAt(pruning_id, newmodule('F')) + s.insertAt(pruning_id+1, newmodule('%')) return s def cut_using_string_manipulation(pruning_id, s, path = None): @@ -78,4 +80,48 @@ def gen_noise_branch(radius,nbp=20): return NurbsCurve([(0,0,0,1),(0,0,1/float(nbp-1),1)]+[(myrandom(radius*amplitude(pt/float(nbp-1))), myrandom(radius*amplitude(pt/float(nbp-1))), pt/float(nbp-1),1) for pt in range(2,nbp)], - degree=min(nbp-1,3),stride=nbp*100) \ No newline at end of file + degree=min(nbp-1,3),stride=nbp*100) + +def create_noisy_circle_curve(radius, noise_factor, num_points=100, seed=None): + if seed is not None: + seed(seed) + t = linspace(0, 2 * pi, num_points, endpoint=False) + points = [] + for angle in t: + # Base circle points + x = radius * cos(angle) + y = radius * sin(angle) + + # Add noise + noise_x = uniform(-noise_factor, noise_factor) + noise_y = uniform(-noise_factor, noise_factor) + + noisy_x = x + noise_x + noisy_y = y + noise_y + + points.append((noisy_x, noisy_y)) + + # Ensure the curve is closed by adding the first point at the end + points.append(points[0]) + + # Create the PlantGL Point2Array and Polyline2D + curve_points = Point2Array(points) + curve = Polyline2D(curve_points) + return curve + +def create_bezier_curve(num_control_points=4, x_range=(0, 10), y_range=(-2, 2), seed_val=None): + if seed_val is not None: + seed(seed_val) # Set the random seed for reproducibility + # Generate progressive control points within the specified ranges + control_points = [] + prev_x = uniform(x_range[0], x_range[1] / 4) + for i in range(num_control_points): + x = prev_x + uniform(0, (x_range[1] - prev_x) / (num_control_points - i)) + y = uniform(*y_range) + control_points.append(Vector3(x, y, 0)) # Set z to 0 for 2D curve + prev_x = x + # Create a Point3Array from the control points + control_points_array = Point3Array(control_points) + # Create and return the BezierCurve2D object + bezier_curve = BezierCurve2D(control_points_array) + return bezier_curve \ No newline at end of file diff --git a/media/envy_model.png b/media/envy_model.png new file mode 100644 index 0000000..4b9bb65 Binary files /dev/null and b/media/envy_model.png differ diff --git a/other_files/apple_model.lpy b/other_files/apple_model.lpy new file mode 100644 index 0000000..f8015d7 --- /dev/null +++ b/other_files/apple_model.lpy @@ -0,0 +1,94 @@ + +import random as rd + +def make_apple_curve(): + y1 = rd.uniform(.65,1) + y2 = rd.uniform(.8,1) + y3 = rd.uniform(.4,.5) + + base_points = [ + #point 0 #point 1 #point 2 #point 3 + (0, 0, 1), (0, y1, 1), (1, y2, 1),(1, y3, 1) + ] + + top_points = [ + (0, y3, 1),(0, 0.118695, 1),(0.909233, 0.146624, 1),(1, -0.0977491, 1) + ] + + base_point_list = Point3Array(base_points) + curve1 = NurbsCurve2D(base_point_list) + base_curve = QuantisedFunction(curve1) + + top_point_list = Point3Array(top_points) + curve2 = BezierCurve2D(top_point_list) + top_curve = QuantisedFunction(curve2) + + return [base_curve,top_curve] + + + +def make_stem_curve(): + + points = [ + (-1.48289, 0.982887, 1),(-0.559816, 1.22861, 1.05),(1.05126, 1.57262, 1),(1.70403, -0.0245722, 1) + ] + ctrlPointList = Point3Array(points) + stem_curve = NurbsCurve2D(ctrlPointList) + return stem_curve + + + + +Axiom: A(.1, .09) + + +derivation length: 2 +production: +A(bh, r): + curves = make_apple_curve() + base_curve = curves[0] + print("BASE CURVE " + str(base_curve) ) + print(base_curve) + top_curve = curves[1] + nproduce SetColor(230,0,0) SectionResolution(60) + produce nF(bh, .01, r, base_curve) ^(180) nF(bh/5, .01, r, top_curve)^(180)S(bh/1.5, r/10) + + +S(sh, r): + stem_curve = make_stem_curve() + nproduce SetColor(100,65,23) + produce SetGuide(stem_curve, sh) _(r)nF(sh, .01, r) + + + +interpretation: + + +endlsystem +###### INITIALISATION ###### + +__lpy_code_version__ = 1.1 + +def __initialiseContext__(context): + import openalea.plantgl.all as pgl + bc = pgl.NurbsCurve2D( + ctrlPointList = pgl.Point3Array([(0, 0, 1),(0, 1.70191, 1),(1, 2.01708, 1),(1, 0.192308, 1)]) , + ) + bc.name = "bc" + parameter = pgl.NurbsCurve2D( + ctrlPointList = pgl.Point3Array([(0, 0.0195083, 1),(0, 0.994923, 1),(0.289506, 1.07946, 1),(1, 0.903884, 1)]) , + ) + parameter.name = "parameter" + import openalea.plantgl.all as pgl + parameter_2 = pgl.BezierCurve2D( + pgl.Point3Array([(0.00549451, 0.00549451, 1),(-0.0018315, 0.204393, 1),(0.106227, 0.243688, 1),(0.106178, 0.117389, 1)]) , + ) + parameter_2.name = "parameter_2" + panel_0 = ({'name': 'Panel 1', 'active': True, 'visible': True},[('Function',bc),('Function',parameter),('Curve2D',parameter_2)]) + parameterset = [panel_0,] + context["__functions__"] = [('bc',bc),('parameter',parameter),] + context["__curves__"] = [('parameter_2',parameter_2),] + context["__parameterset__"] = parameterset + context["bc"] = pgl.QuantisedFunction(bc) + context["parameter"] = pgl.QuantisedFunction(parameter) + context["parameter_2"] = parameter_2 diff --git a/other_files/leaf_model.lpy b/other_files/leaf_model.lpy new file mode 100644 index 0000000..ce1afd3 --- /dev/null +++ b/other_files/leaf_model.lpy @@ -0,0 +1,49 @@ +import random as rd +def make_leaf_guide(): + + x1 = rd.uniform(0,.25) + x2 = rd.uniform(.65,.85) + y1 = rd.uniform(.45,.55) + y2 = rd.uniform(.25,.35) + + control_points = [ + Vector3(0,0,0), + Vector3(x1,y1,0), + Vector3(x2,y2,0), + Vector3(1,0,0), + ] + + inverse_points = [ + Vector3(0,0,0), + Vector3(x1,-y1,0), + Vector3(x2,-y2,0), + Vector3(1,0,0), + ] + + side1 = Point3Array(control_points) + side2 = Point3Array(inverse_points) + + curve1 = BezierCurve2D(side1) + curve2 = BezierCurve2D(side2) + + return [curve1, curve2] + +Axiom: L + +derivation length: 1 +production: + +L: + nproduce SetColor(0,225,0) + + curves = make_leaf_guide() + curve1 = curves[0] + curve2 = curves[1] + + produce _(.0025) F(.1){[SetGuide(curve1, 1) _(.001).nF(1, .1)][SetGuide(curve2, 1)_(.001).nF(1, .1)]}(True) + + +interpretation: + + +endlsystem