The Artist's Husband: Let's Make Some Noise!

/2024/02/the-artists-husband-lets-make-some-noise/images/top_image.png

Sometimes you just want to make some noise! In drawing terms, that means you don’t want a boring straight line; you want a straightish squiggly line which looks hand-drawn. Or you don’t want a flat surface, you want it to have some texture. You can generate these kinds of effects by adding some random noise, but how? There are a couple of ways to do this. We’ll cover some simple examples here.

Suppose we want to generate something that looks like a mountain range in the distance. We could take a straight horizontal line and randomly vary its elevation from one pixel to the next. Let’s give that a try! Here is a Nannou app that moves along the x-axis and plots random values in the y-axis:

use nannou::prelude::*;

fn main() {
    nannou::app(model)
        .update(update)
        .run();
}

struct Model {
    points: Vec<Point2>,
}

fn model(app: &App) -> Model {
    app
        .new_window()
        .size(600, 200)
        .view(view)
        .build()
        .unwrap();

    Model {
        points: Vec::new(),
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let rect = app.window_rect();
    if model.points.len() < rect.w() as usize {
        let step = (model.points.len() + 1) as f32;
        let amplitude = random_f32();
        model.points.push(pt2(step, amplitude));
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    let rect = app.window_rect();
    let draw = app.draw();

    for p in &model.points {
        let y = map_range(p[1], 0., 1.,
                          rect.top() - 10., rect.bottom() + 10.);
        draw.ellipse().x(p[0] + rect.left()).y(y).w_h(1.0, 1.0);
    }

    draw.to_frame(app, &frame).unwrap();
}

The latest version of this app can be found at https://gitlab.com/theartistshusband/noise/-/tree/main/random.

This app draws a graph that looks like this:

/2024/02/the-artists-husband-lets-make-some-noise/images/random.png

Well, that’s not particularly helpful, is it?

While pretty much any programmable system has a way of generating random numbers, generally these are evenly distributed, meaning that any number in the range you are generating numbers for is just as likely as any other number. Our graph doesn’t look like a mountain range because there is no continuity; the slightest move to the left or right could put you at an extremely different elevation. While real mountains do have steep cliffs, it’s more likely that if you take a single step, you will go slightly more up or down hill.

So what we really want to have is a slight random change in y for a small change in x, but to allow for a large change in y for a large change in x. There are ways to do this! One way is Perlin Noise , invented by Ken Perlin in 1983. Other methods include Improved Perlin Noise and Simplex Noise (also invented by Ken Perlin.) If you are interested in the nitty-gritty details of how this works, there is an excellent discussion of Improved Perlin Noise at https://adrianb.io/2014/08/09/perlinnoise.html.

To modify the Nannou app to use a better noise algorithm to fit our use case, we need to store a noise generator in the Model, initialize it in model(), and use it in update(). Here are the major changes to the code:

struct Model {
    noise: BasicMulti,
    points: Vec<Point2>,
}

fn model(app: &App) -> Model {
    app
        .new_window()
        .size(1000, 600)
        .view(view)
        .build()
        .unwrap();

    Model {
        noise: BasicMulti::new().set_seed(random()),
        points: Vec::new(),
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let rect = app.window_rect();
    if model.points.len() < rect.w() as usize {
        let step = (model.points.len() + 1) as f32;
        let amplitude = model.noise.get([step as f64 / 400., 0.]) as f32;
        model.points.push(pt2(step, amplitude));
    }
}

There other minor changes, such as importing new dependencies. See https://gitlab.com/theartistshusband/noise/-/tree/main/one_dim for the full app.

The particular noise type we are using in this case is BasicMulti, which is much like Perlin Noise, with slightly different algorithm that dampens high frequency changes as the output approaches zero. The effect this has is that the line is fairly smooth at zero and more “jagged” at positive and negative values further from zero. This bodes well for a better looking outline of a mountain range, which is our goal here.

Here is what the output of this Nannou app looks like:

/2024/02/the-artists-husband-lets-make-some-noise/images/one_dim.png

That looks a lot more like a mountain range!

So what can you do with this? If your goal really was to generate something that looks like the outline of a mountain range, there you go! But you can use this any place you might need a generate a small but random change. Consider the Drunken Lines post from last week. Suppose we used this kind of noise to choose the number of degrees to change the direction instead of allowing all directions to have an equal probability. The line would tend to move in spirals, which would be an interesting effect. Or suppose you had several points on the screen and used this noise to decide how they would move. They would tend to move in the same way, but not exactly the same way. We’ll explore some patterns based on these ideas in a future post.

What we talked about here is one-dimensional noise. Suppose you want to add texture to a surface. For that you need two-dimensional noise, which will be the topic of next week’s post.