Raphaël HUCHET

https://raph.site

A Procedural Landscape Experiment

Quick and dirty(?) procedural generation in 99 LoC of Rust

Some days ago, I tried to create a procedural landscape generator. Here is the actual result: https://rap2hpoutre.github.io/landscape-site/
I like to know how things work; this is how I built that one.

Mountains

I’m not a good programmer and I don’t know how to create mountains. I found this answer on Stack Exchange: https://gamedev.stackexchange.com/a/93531/81351
So, I transposed this code into Rust, because I do like Rust for reasons. First, I created a
Mountain
struct.
struct Mountain { 
    points: Vec<u32>,
}
The
points
are the “y” position of each summit: on a 640x480 image, there are 640 points varying from 0 (highest) to 480 (lowest).
Then I created two associated functions: a
new
method to initialize points and a
draw
method to render each pixel of the mountain range.
impl Mountain {
    fn new(y_amp: (f64, f64)) -> Mountain {
        let mut rng = rand::thread_rng();
        let step_max = rng.gen_range(0.9, 1.1);
        let step_change = rng.gen_range(0.15, 0.35);
        let (height_min, height_max) = y_amp;
        let mut height = rng.gen_range(0.0, height_max);
        let mut slope = rng.gen_range(0.0, step_max) * 2.0 - step_max;
        let mut points: Vec<u32> = Vec::new();

        for _ in 0..640 {
            height = height + slope;
            slope = slope + (rng.gen_range(0.0, step_change) * 2.0 - step_change);

            if slope > step_max {
                slope = step_max;
            } else if slope < -step_max {
                slope = -step_max;
            }

            if height > height_max {
                height = height_max;
                slope = slope * -1.0;
            } else if height < height_min {
                height = height_min;
                slope = slope * -1.0;
            }
            points.push(height as u32);
        }
        Mountain {
            points: points
        }
    }
    fn draw(&self, img: &mut RgbImage, color: Rgb<u8>, c_fog: Rgb<u8>) {
        let mut i = 0;
        for &point in self.points.iter() {
            img.put_pixel(i, point, color);
            for j in point..480 {
                img.put_pixel(i, j, interpolate(c_fog, color, j as f32 / 480.0));
            }
            i = i + 1;
        }
    }
}
It’s the longest part (it’s dirty, not the ugliest though!), think it’s just an adapted copy/paste from the StackExchange post I mentioned.
The
new
method takes a tuple of arguments
y_amp
which is just the bounds of the current mountain range (max height and min height). The points are initialized with random adjacent values.
The
draw
method takes a reference to the image we are drawing on and two colors: the initial color of the mountain range (
color
) and the color of the fog (
c_fog
) which is required in order to create a gradient. For each point, draw a gradient “line” form the summit to the bottom of the image, mixing fog color with initial color.
I could have use constants (for 640, 480, etc.), choose better name for variables and add comments but, hey! No time! Be quick, be dirty.

Random color helper

The main task of this program is to generate random colors. I created a small helper to generate a color:
fn rgb_rand(rng: &mut ThreadRng, r: (u8, u8), g: (u8, u8), b: (u8, u8)) -> Rgb<u8> {
    Rgb([rng.gen_range(r.0, r.1), rng.gen_range(g.0, g.1), rng.gen_range(b.0, b.1)])
}
I forgot to mention that all functions and structs like
Rgb
,
RgbImage
,
interpolate
, etc. comes from the excellent, partially documented and more or less stables “image” and “imageproc” crates (a crate is a Rust lib).
The
rgb_rand
function I wrote can generate a random color from red range, green range and blue range. I will use it everywhere from now.

Gradient sky and random moon

With the help of
rgb_rand
function, I can initialize a random sky color, a random fog color, and a random moon color.
The sky. It can be light, dark or light-blue, let’s throw 1D3:
let mut rng = rand::thread_rng();
let c_sky = match rng.gen_range(1, 4) {
    1 => rgb_rand(&mut rng, (1, 40), (1, 40), (1, 40)),
    2 => rgb_rand(&mut rng, (215, 225), (215, 225), (230, 255)),
    _ => rgb_rand(&mut rng, (200, 255), (200, 255), (200, 255)),
};
The fog has a totally random color and the planet (moon? sun? who cares?) color is a mix between the sky and a random color:
let c_fog = rgb_rand(&mut rng, (1, 255), (1, 255), (1, 255));
let c_planet = interpolate(rgb_rand(&mut rng, (1, 255), (1, 255), (1, 255)), c_sky, 0.1);
Now let’s draw everything. So I started with the sky (our base image buffer):
let mut img = ImageBuffer::from_pixel(640, 480, c_sky);
Then the program draws the planet as often as not after throwing a coin with
gen_weighted_bool
. The planet is just a filled circle. Sometimes, another filled circle is drawn just after with the color of the sky to create a crescent moon effect.
if rng.gen_weighted_bool(2) {
    let x = rng.gen_range(101, 520);
    let y = rng.gen_range(81, 200);
    let rad = rng.gen_range(20, 80);
    draw_filled_circle_mut(&mut img, (x, y), rad, c_planet);
    if !rng.gen_weighted_bool(5) {
        draw_filled_circle_mut(&mut img, (x + rng.gen_range(-2, 4) * 10, y), rad, c_sky);
    }
}
Then a gradient is applied on everything we drawn.
for (_, y, pixel) in img.enumerate_pixels_mut() {
    *pixel = interpolate(c_fog, *pixel, y as f32 / 1000.0);
}
With all this code in a main function, we now have a gradient sky with a moon.

Muted mountains on a gradient sky

It’s time to add some mountains ranges on this gradient sky.
let mountain_count: u32 = rng.gen_range(4, 7);
let c_mountain = rgb_rand(&mut rng, (1, 255), (1, 255), (1, 255));
for i in 0..mountain_count {
    let c = interpolate(c_mountain, c_sky, (i + 1) as f32 / mountain_count as f32);
    let y_amp = ( (399 - 480 / 2 / mountain_count * (mountain_count - i)) as f64, 401.0 );
    Mountain::new(y_amp).draw(&mut img, c, c_fog);
}
In short, I just defined a random number of mountain range
mountain_count
, then a base color
c_mountain
. Then we iterate for each mountain range, build and draw the
Mountain
we created on the first step.
The deep down mountains have their color softened with the sky color. Remember the fog color is also mixed with the mountains to make them seems less flat.
Maybe I forgot something, the whole code is available here: https://github.com/rap2hpoutre/landscape/blob/master/src/main.rs

Final thoughts

I love procedural generation and I love to read about how procedural things were created, even if I don’t understand most of the time. This post is a description of what I coded, it’s obviously not a how-to or a tutorial about Rust. The result is not super-sexy, but I liked creating it. I also built a small web site with a fresh landscape generated every minute. In a frame, because I wanted it look like artistic! Here it is: https://rap2hpoutre.github.io/landscape-site/
My main inspiration were Navarre mountains. Here is a list of projects far better than mine, also I plundered their ideas:
I would be happy to answer questions, listen to suggestions or face criticisms.
Thanks to @dorhan_ for his review!

Tags

More by Raphaël HUCHET

Topics of interest