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:
Of the first 100 seeds, about a third of them produce non-trivial shapes. Here's a sampling with varying recursion depths:
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]}}
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]}}
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.