Saturday, March 31, 2018

A family of fractal trees

Here is a new two-parameter family of fractal trees. The square of the width follows a Cantor function parameterised by the Cantor ratio c, which is 3/5 here:
the ratio c must be more than 0.5, with large values (here 0.75) giving wide, straight branches: 
and smaller values (here 0.52) giving thin, meandering branches:
The tree is built entirely from triangles which are all junction points. Here I show in white the largest of these triangles (c=0.525) which are all right-angled, making it a relative of the Pythagoras tree:

The right-angled triangles mean the tree obeys Leonardo's rule, maintaining total cross-sectional area if you assume a 3rd dimension for the branches. Like the Pythagoras tree, branches can overlap meaning the tree is immersed in (but not embedded in) 2D space. 

The branching is done by repeatedly splitting the length in half with alternating branching direction. Consequently a single branch is a relative of the Koch curve but with bend angle reducing towards the stem:
It is also possible to tweak the algorithm, for instance by flipping the direction of the offshoot branch:
however this gives very curly results, and I think of tweaks like this as non-standard variations of this tree family. 

The preserving of branch widths to power 2 is defined as the Pythagoras power p=2. But it doesn't have to be 2. "For real trees, the exponent in the equation that describes Leonardo's hypothesis is not always equal to 2 but rather varies between 1.8 and 2.3 depending on the geometry of the specific species of tree." Leonardo's rule.
So p is the second adjustable parameter. If we reduce p to 1.4 we get:
making the triangles white we see that they are now less than 90 degree triangles:
Though they still tend to right angles triangles on the smallest offshoots compared to trunk width. 
We can reduce the width by reducing c (from 0.52) as it now can be less than half. Here we set it to 0.25:
Not only is the fork angle less when p is less than 2, but the small branches take up less proportion. Taken to the extreme we get an almost bare tree, (p=1.2, c=0.07):

As you can see, the tree width is affected by both c and p. It is easier to parameterise instead by the tree gradient and p. The gradient is: 
So c can be found to give the required gradient. Now a plot can be made of the 2D space of trees:
Here the triangle edges are drawn in grey. 

This 2D family has a number of sub-families of trees. There is of course the family of zero-area trees, which would sit just above the top row, with gradient=0. There is also the family of trees with symmetric central fork, I'll call these Cantor trees as they also use the exact one-third Cantor ratio c=1/3:
The intersection of these two sub-families is seen at the top of the above image. We might call this the Cantor fractal tree. 

So three sub-families are the thin (gradient 0) trees, the p=2 family (which we might call the Leonardo trees as they obey Leonardo's rule) and the Cantor trees above. A fourth subfamily is the family of balanced trees. These won't fall left or right. A property that is true of all their branches as well, if you pulled them off and stood them up. For low gradient trees the Pythagoras constant seems to be around 1.8:

Here is the family, the p values are approximate. Notice that the dominant branch moves from the left to the right branch as the tree gradient gets higher:

As such the balanced trees cross with the Cantor trees. The intersecting balanced Cantor tree has gradient of about 0.32:

Pseudocode

Even though these structures are built from just triangles, we can't perform infinite iterations, so to avoid little gaps we model each section of the tree as a pentagon, that tends to a triangle with increasing iterations. We define the section in the coordinate frame of its parent:
struct Section
{
  Vector2d pos, peak;
  Vector2d xAxis, yAxis;
  double width, width2, length;
  double dir; // sign value sets which side the first branch is on
  vector < Section > children;

  void split();
};

We now define an initial tree section:
  static double p = 2;   // Pythagoras exponent
  static double C = 3/5; // Cantor scale
  double gradient = (2 - 4*pow((1 - c) / 2, 1 / p)) / pow(c, 1/p);
  Section base;
  base.xAxis = Vector2d(1, 0);
  base.yAxis = Vector2d(0, 1);
  base.length = 1.0;
  base.width = base.length * gradient;
  base.width2 = 0;
  base.pos = Vector2d(0, 0);
  base.dir = 1.0;

Then call:
  base.split();

which recursively splits the tree in half and adds branches until the minimum section length is met:
void Section::split()
{
  double minLength = 0.005;
  if (length <= minLength)
    return;
  Section child1, child2;

  double wp = pow(width, p);
  double wp2 = pow(width2, p);
  double areaLoss = (wp - wp2) * C;
  double a = pow(areaLoss, 1/p);
  double b = pow((wp + wp2 - areaLoss)/2, 1/p);
  double c = pow((wp + wp2 + areaLoss)/2, 1/p);
  double theta = acos((sqr(b) + sqr(c) - sqr(a)) / (2*b*c));
  Vector2d apex(c/2 - b*cos(theta), b*sin(theta));
  Vector2d B(-c/2, 0);
  Vector2d A(c/2, 0);
  Vector2d fromB = apex - B;
  Vector2d fromA = apex - A;
  length *= pow((1 - C) / 2, 1/p);

  child1.pos = B + fromB/2;
  child1.yAxis = Vector2d(-fromB[1], fromB[0]).normalized();
  child1.xAxis = fromB.normalized();
  child1.pos[0] *= dir; 
  child1.yAxis[0] *= dir; 
  child1.xAxis[1] *= dir;
  child1.length = a / gradient; 
  child1.width = a;
  child1.width2 = 0;
  child1.dir = -dir; // remove negation for curly tree version
  child1.peak = Vector2d(0, 0);

  child2 = *this; 
  child2.pos = A + 0.5*fromA;
  child2.yAxis = Vector2d(fromA[1], -fromA[0]).normalized();
  child2.xAxis = -fromA.normalized();
  child2.pos[0] *= dir; 
  child2.yAxis[0] *= dir; 
  child2.xAxis[1] *= dir;
  child2.width = b;
  child2.dir = -dir;

  peak = apex; 
  peak[0] *= dir;
  width2 = c;

  children.clear();
  children.push_back(child1);
  children.push_back(child2);

  for (auto& c : children)
    c.split();
  dir = -dir;
  split();
}

The code plus a simple svg based render to file is here

No comments:

Post a Comment