Frost Patterns
things to make and do on a train
people also said it looks like randomly generated dungeons or mycelium
I learned about an algorithm called Diffusion-limited aggregation from this youtube video about procedural landscape generation for games.
The process looks like this:
It happens on an empty grid with one cell in the middle. Let's call that cell the shape. Then, in each next step of the algorithm, a new cell is spawned in a random location and does a random walk until it touches a side of the shape, after which it stays there and becomes a part of the shape.
Me infodumping about this to Shebang
cause copying dms is more fun than writing an article
we ended up generating the account header for shebang's fedi account with this.
Implementation
I published the code in this git repo: frost_patterns.
It builds into an executable with a cli interface for easy image generation. All information about it is in the README.md
file.
Minimal example
I wrote it in Rust, here is the DLA algorithm for generating the grid:
fn generate_grid() -> [[bool; HEIGHT]; WIDTH] {
let mut rng = ChaCha8Rng::seed_from_u64(SEED);
let mut grid: [[bool; HEIGHT]; WIDTH] = [[false; HEIGHT]; WIDTH];
grid[WIDTH / 2][HEIGHT / 2] = true;
for particle_i in 0..NUM_PARTICLES {
// place the particle randomly
let mut x: isize = rng.gen_range(0..WIDTH).try_into().unwrap();
let mut y: isize = rng.gen_range(0..HEIGHT).try_into().unwrap();
// replace it randomly until it's on an empty space
while is_stuck((x, y), &grid) || grid[x as usize][y as usize] {
x = rng.gen_range(0..WIDTH).try_into().unwrap();
y = rng.gen_range(0..HEIGHT).try_into().unwrap();
}
// random walk until it attaches to the main shape
while !is_stuck((x, y), &grid) {
let diff_x: isize = rng.gen_range(-1..=1);
let diff_y: isize = rng.gen_range(-1..=1);
if x + diff_x < WIDTH as isize
&& x + diff_x >= 0
&& y + diff_y < HEIGHT as isize
&& y + diff_y >= 0
&& !grid[(x + diff_x) as usize][(y + diff_y) as usize]
{
x += diff_x;
y += diff_y;
}
}
// make it part of the main shape
grid[x as usize][y as usize] = true;
// the generation takes a while usually, it's
// a good idea to have an idea of the progress
println!("particle {:5>}/{}", particle_i + 1, NUM_PARTICLES);
}
grid
}
// checks if any neighbors of the cell are part of the shape
fn is_stuck(xy: (isize, isize), grid: &[[bool; HEIGHT]; WIDTH]) -> bool {
let (x, y) = xy;
if x - 1 >= 0 && grid[usize::try_from(x - 1).unwrap()][usize::try_from(y).unwrap()] {
return true;
}
if x + 1 < WIDTH.try_into().unwrap()
&& grid[usize::try_from(x + 1).unwrap()][usize::try_from(y).unwrap()]
{
return true;
}
if y - 1 >= 0 && grid[usize::try_from(x).unwrap()][usize::try_from(y - 1).unwrap()] {
return true;
}
if y + 1 < HEIGHT.try_into().unwrap()
&& grid[usize::try_from(x).unwrap()][usize::try_from(y + 1).unwrap()]
{
return true;
}
return false;
}
You can start using this code just by printing out the grid like how I did at the start.
fn print_grid(grid: [[bool; HEIGHT]; WIDTH]) {
for y in 0..HEIGHT {
for x in 0..WIDTH {
match grid[x][y] {
true => print!("X"),
false => print!("."),
}
}
println!();
}
}