Heightmap Generator
A Rust + Slint tool for procedurally generating and eroding heightmaps.
Overview
In the very beginnings of me, learning rust, I chose a Heightmap generator as a learning project. The goal was less the visual output than to learn how to construct a GUI with the help of Slint and get a little bit more into the concept of rust. I got carried away with the little improvements, but I’m happy with the output overall.
To display the terrain in 3d, I made a little CPU based edge renderer rendering a displaced plane into an image and embedding it into the GUI.
My role: Sole author; a personal project to deepen my Rust and Slint GUI skills.
Gallery
Features
- Layering of perlin noise either blending, screening or multiplying
- Thermal erosion
- River flow generation
- Generate flattened water planes
- Import/export from/to desktop
- CPU based rendering of the terrain in 3d
Deep Dive
Basic Buffer blending:
pub fn blend_buffers(buffer_a: &ImageBuffer<Rgba<u8>, Vec<u8>>, buffer_b: &ImageBuffer<Rgba<u8>, Vec<u8>>, blend_mode: i32) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let (width, height) = buffer_a.dimensions();
ImageBuffer::from_fn(width, height, |x, y| {
let pixel_a = buffer_a.get_pixel(x, y);
let pixel_b = buffer_b.get_pixel(x, y);
let alpha_a = pixel_a[3] as f32 / 255.0;
let alpha_b = pixel_b[3] as f32 / 255.0;
let new_alpha = alpha_a * (1.0 - alpha_b) + alpha_b;
let mut blended_pixel = [0u8; 4];
for i in 0..3 {
let channel_a = pixel_a[i] as f32 / 255.0;
let channel_b = pixel_b[i] as f32 / 255.0;
match blend_mode {
// Blend
0 => {
blended_pixel[i] = ((channel_a * (1.0 - alpha_b) + channel_b * alpha_b) * 255.0).min(255.0) as u8;
},
// Multiply
1 => {
let blended_channel = channel_a * (1.0 - alpha_b) + channel_a * channel_b * alpha_b; // Interpolate based on alpha_b
blended_pixel[i] = (blended_channel * 255.0).min(255.0) as u8;
},
//
2 => {
blended_pixel[i] = ((channel_a * alpha_a + channel_b * alpha_b) * 255.0).min(255.0) as u8;
},
_ => {
panic!("Invalid blend mode");
}
}
}
blended_pixel[3] = (new_alpha * 255.0).min(255.0) as u8;
Rgba(blended_pixel)
})
}
The thermal erosion:
pub fn thermal_erosion(
heightmap: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
colormap: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
iterations: usize,
talus_angle: f32,
erosion_mode: i32,
) {
let (width, height) = heightmap.dimensions();
for _ in 0..iterations {
let temp_heightmap = heightmap.clone(); // Temporary heightmap to store updates
let temp_colormap = colormap.clone(); // Temporary colormap to store updates
for y in 1..(height - 1) {
for x in 1..(width - 1) {
let center_pixel = temp_heightmap.get_pixel(x, y);
let mut changed = false;
for dx in -1..=1 {
for dy in -1..=1 {
if dx == 0 && dy == 0 {
continue;
}
let neighbor_pixel = temp_heightmap.get_pixel((x as i32 + dx) as u32, (y as i32 + dy) as u32);
let neighbor_color = temp_colormap.get_pixel((x as i32 + dx) as u32, (y as i32 + dy) as u32);
if should_erode(*center_pixel, *neighbor_pixel, talus_angle, erosion_mode) {
// Update both heightmap and colormap
heightmap.put_pixel(x, y, *neighbor_pixel);
colormap.put_pixel(x, y, *neighbor_color);
changed = true;
break;
}
}
if changed {
break;
}
}
}
}
}
}