This post is going to be more about Rust than about art. You have been warned!
As I was working on moving the Hyphae code into TAHGA Lib , I found too much complexity and repetition in what I had done before. What I was doing to modularize it for the library was making matters worse.
The problem was that I needed a structure to define a hypha, a single strand that grows over time until it dies. Then I needed a structure, to define a list of hyphae (for those of you who are confused by these terms: hyphae is the plural of hypha.) The Hyphae struct would not only hold the list of Hypha
instances, but some housekeeping information as well. In particular, I wanted to set various parameters there which would affect how the hyphae would grow (for example, a initial growth speed, a maximum speed, etc.) So the Hyphae
structure would provide a method to generate a new Hypha
struct and add it to the list of hyphae.
This is all well and fine, but a Hypha
struct also needs to be able to create a new Hypha
. A Hypha
struct doesn’t see all the parameters in the Hyphae
struct. If I try to pass a reference to the Hyphae
into each Hypha
struct so the can see those parameters, I’ll run afoul of some memory safety rules: I’ll never be able to modify the Hyphae
struct when each Hypha
struct contains a read-only reference to it. If you’re using some language other than Rust, that is not that hard, but it’s up to you to make sure you don’t try to read anything out of the Hyphae
struct while it is being modified. Rust won’t let you even try to pretend you can do that without messing it up; it just won’t allow the situation to come up. So how can I allow a Hypha
struct to create another Hypha
struct using parameters defined in the Hyphae
struct?
My solution was to use a third struct, called Factory
, which knows the parameters and can generate a Hypha
. You can see the implementation as it stands so far in
GitLab
.
To use this, do something like this as your model()
:
fn model(app: &App) -> Model {
app
.new_window()
.size(WIDTH, HEIGHT)
.view(view)
.key_pressed(key_pressed)
.key_released(key_released)
.build()
.unwrap();
let root_count: usize = 4;
let factory = Factory::new();
let mut hyphae = Hyphae::new_with_factory(factory);
for i in 0..root_count {
let position = vec2(
random_range(-(WIDTH as f32 / 2.0) + 100., WIDTH as f32 / 2.0 - 100.),
random_range(-(HEIGHT as f32 / 2.0) + 100., HEIGHT as f32 / 2.0 - 100.),
);
hyphae.add_hypha(
hyphae.new_hypha()
.position(position)
.color(hsla(i as f32 / root_count as f32, 1., 0.03, 1.0))
);
}
Model {
hyphae,
paused: false,
ctrl_key_pressed: false,
}
}
This creates a Factory
with all default values, which is used to create the Hyphae
structure. Then we create four Hypha
structures by making calls to hyphae.new_hypha()
, This method creates a new Hypha
and applies all the parameters the Factory
knows about. You are then free to change even more parameters using the builder pattern. In this case, we are setting a (random) position and a unique color. We use hypahe.add_hypha()
to add each of these to the Hyphae
structure.
The Factory did a lot of work for you behind the scenes. There are, at the time of this writing, 13 Hypha
parameters it modifies, but as long as you like the defaults, you don’t have to change any of them; you only need to change the ones you want to change.
By the way, if you don’t change any of the parameters, the default Factory setting will create Hypha
instances that act very much like
the ones we created a few weeks ago
.
Our original problem was that Hypha
instances need to have the ability to create new Hypha
instances (this is where the branches come from.) Because we have a Factory
object, this now works. The Hyphae
instance calls the update()
method on each Hypha
instance it knows about like this:
// Perform a cycle of growth this hypha
// This potentially yields a new, branched hypha
let branch_hypha = hypha.update(&self.factory);
As you can see, it passes in the Factory
object, which is used to create any ny Hypha
instances.
As I mentioned, there are 13 parameters the Factory knows about. These include line weight, growth speed, and the amount of variation allowed in the direction, as well as minimum and maximum values for these, and instructions for how these values should change over time. Modifying some of these will produce very subtle changes in the output. Others will produce large changes. The parameters may interact in some surprising ways. For example, increasing the probability of a Hypha
producing a branch when line weight is large may produce fewer visible branches, since the branches will be so close to each other they will overlap, then immediately die for the sin of touching another Hypha
.
To modify a factory, use the builder pattern. In this example, we’ll change the amount the direction is allowed to vary to 0
.
let factory = Factory::new()
.fn_direction_variance(|| 0.);
This should result in a lot of straight lines, with branches always being a right angles. Here is the result if the same program we used to create the image above with just that change.
But wait! What is this:
.fn_direction_variance(|| 0.);
We wanted the direction variance to be 0
, but what is || 0
all about?
Ahh, you have hit on an excellent question! Technically, fn_direction_variance()
doesn’t accept a value, in this case a 32-bit floating point value, or f32
. It accepts a “function that accepts no value, and which returns an f32
”. And, even more technically, that isn’t quite correct either; it’s accepting a closure, not a function. You can read more about closures in the Rust docs; they are useful for way more than the trivial use we are putting them to here. In this case, you may think of this as a passing a function as a parameter. That function will be called later to produce the value we want.
So fn_direction_variance(|| 0.)
is setting the direction_variance
parameter to a function that always returns 0
.
Why not just set it to 0
if that is what we want? Why, oh why go through all this extra complexity just to get a 0
? Are you just doing this to mess with my head??
Calm down, There is actually a good reason for this. The Factory
is used to produce new Hypha
instances for whatever you need them for. You might not want all the those Hypha
instances to be identical, which they would be if the Factory
produced individual values for each parameter. You might just want them to be similar, but to have some variation between them. The Factory
dealing in functions instead of values allows for this. So fn_direction_variance(|| 0.)
always sets the direction variance to 0
for every Hypha
, but fn_direction_variance(|| random_range(0.4, 0.8))
will set each Hypha
’s direction_variance to a random value between 0.4
and 0.8
(this is actually the default function for direction variance.) These functions can be as arbitrary as you like. If your application really needs to have a parameter be between 0.1
and 0.35
or 0.95
, unless it’s after 2pm, in which case always set it to 0.5
, knock yourself out.
The functions we are allowing here are pretty simplistic. The type is fn() -> f32
, which means they take no values and return an f32
. There is no reason the functions could not be much more complex, perhaps accepting values to act upon, or possibly making decisions based on values of several parameters.
For those of you, if any, still reading this, thanks for sticking with it! It will be important to understand if you are going to use the TAHGA library.
If you are using another language, most have something like the ability to pass functions as parameters like we are doing here. Look for anonymous functions or lambdas, or possibly pointers to functions in your language.
Next week, I’ll try to do something more “arty”! I promise!