exupero's blog
RSSApps

Generating Lindenmeyer systems

One advantage of defining a Lindenmeyer system using data rather than executable logic is that data is much easier to generate, allowing us to create new, procedurally generated fractals quite easily.

To start with, here are versions of clojure.core's rand-int and rand-nth that take a random number generator, in order to reproduce results from a constant seed:

(defn rng-rand-int
  ([rng bound]
   (.nextInt rng bound))
  ([rng low high]
   (+ low (rng-rand-int rng (- high low)))))
(defn rng-rand-nth [rng coll]
  (nth coll (rng-rand-int rng (count coll))))

With these functions we can reproducibly generate random L-system definitions using the code below, which defines two rules that expand into between 1 and 10 steps:

(defn generate-system [rng]
  {:axiom '[A]
   :rules {'A (repeatedly (rng-rand-int rng 1 10) #(rng-rand-nth rng '[A B F + -]))
           'B (repeatedly (rng-rand-int rng 1 10) #(rng-rand-nth rng '[A B F + -]))}
   :moves '{+ [:turn 90]
            - [:turn -90]
            F [:forward 1]}})

A random seed of 0 produces the following definition:

(generate-system (java.util.Random. 0))
{:axiom [A],
 :rules {A (- F A + B B -), B (- F F + F A - -)},
 :moves {+ [:turn 90], - [:turn -90], F [:forward 1]}}

And recursing eight levels deep generates this design:

generated-l-system.png

Of the first 100 seeds, about a third of them produce non-trivial shapes. Here's a sampling with varying recursion depths:

Seed 3, depth 9Seed 4, depth 7Seed 6, depth 7Seed 14, depth 7Seed 23, depth 6Seed 29, depth 10Seed 34, depth 10Seed 36, depth 7Seed 37, depth 6Seed 38, depth 8Seed 48, depth 10Seed 51, depth 10Seed 53, depth 6Seed 65, depth 8Seed 70, depth 9Seed 77, depth 8Seed 87, depth 8Seed 91, depth 9Seed 97, depth 9Seed 99, depth 10

Let's make this more interesting by trying different turn angles:

(defn generate-system-b [rng]
  (let [angle (rng-rand-nth rng [15 30 45 60 75 90 105 120 135 150])]
    {:axiom '[A]
     :rules {'A (repeatedly (rng-rand-int rng 1 10) #(rng-rand-nth rng '[A B F + -]))
             'B (repeatedly (rng-rand-int rng 1 10) #(rng-rand-nth rng '[A B F + -]))}
     :moves {'+ [:turn angle]
             '- [:turn (- angle)]
             'F [:forward 1]}}))
(generate-system-b (java.util.Random. 0))
{:axiom [A],
 :rules {A (F A + B B - - F), B (F + F A -)},
 :moves {+ [:turn 15], - [:turn -15], F [:forward 1]}}
generated-l-system-b.png

Also, let's step forward by more than one distance:

(defn generate-system-c [rng]
  (let [angle (rng-rand-nth rng [15 30 45 60 75 90 105 120 135 150])]
    {:axiom '[A]
     :rules {'A (repeatedly (rng-rand-int rng 1 10) #(rng-rand-nth rng '[A B Fa Fb + -]))
             'B (repeatedly (rng-rand-int rng 1 10) #(rng-rand-nth rng '[A B Fa Fb + -]))}
     :moves {'+ [:turn angle]
             '- [:turn (- angle)]
             'Fa [:forward (rng-rand-int rng 1 5)]
             'Fb [:forward (rng-rand-int rng 1 5)]}}))
(generate-system-c (java.util.Random. 12))
{:axiom [A],
 :rules {A (B + - Fa B Fa), B (Fa - B B A A - Fb)},
 :moves
 {+ [:turn 105], - [:turn -105], Fa [:forward 1], Fb [:forward 2]}}
generated-l-system-c.png

Increasing the degrees of freedom a generated system has makes interesting designs rarer and rarer, so in the next post I'll tinker with ways we might pick more reliable expansion rules, inspired by known, regular fractals.