use std::{
cmp::{max, min},
path::Path,
};
use bitflags::bitflags;
use image::{
DynamicImage, GrayImage, ImageBuffer,
imageops::{FilterType, resize},
};
use rand::{RngExt, rngs::StdRng, seq::IndexedRandom};
use crate::grid::{
Cell, Grid, Size, WrapFlags, direction::Direction, offset_coordinate::OffsetCoordinate,
};
const DEFAULT_WIDTH_EXP: u32 = 7;
const DEFAULT_HEIGHT_EXP: u32 = 6;
pub struct CvFractal<G: Grid> {
map_size: Size,
fractal_grid: G,
flags: FractalFlags,
fractal_exp: FractalExp,
fractal_array: Vec<Vec<u32>>,
}
impl<G: Grid> CvFractal<G> {
fn empty(grid: G, flags: FractalFlags, fractal_exp: FractalExp) -> Self {
let map_size = grid.size();
let fractal_width = fractal_exp.fractal_width();
let fractal_height = fractal_exp.fractal_height();
let fractal_grid = grid.with_dimensions(fractal_width, fractal_height);
let fractal_array =
vec![vec![0; (fractal_height + 1) as usize]; (fractal_width + 1) as usize];
Self {
map_size,
fractal_grid,
fractal_array,
flags,
fractal_exp,
}
}
fn generate_fractal(
&mut self,
random: &mut StdRng,
grain: u32,
hint_image: Option<&DynamicImage>,
rifts: Option<&CvFractal<G>>,
) {
let fractal_exp = self.fractal_exp;
let fractal_width = fractal_exp.fractal_width();
let fractal_height = fractal_exp.fractal_height();
let max_allowed = 7.min(fractal_exp.width_exp).min(fractal_exp.height_exp);
let grain = if grain > max_allowed {
eprintln!(
"Warning: grain value {} exceeds maximum allowed value {}. Clamping to {}.",
grain, max_allowed, max_allowed
);
max_allowed
} else {
grain
};
let smooth = (max_allowed - grain) as usize;
let hint_width = (fractal_width >> smooth)
+ if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapX) {
0
} else {
1
};
let hint_height = (fractal_height >> smooth)
+ if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapY) {
0
} else {
1
};
if let Some(img) = hint_image {
let gray_hint_img = if hint_width != img.width() || hint_height != img.height() {
eprintln!(
"Image size {}x{} doesn't match hint size {}x{}.
We will resize the image to the hint size.
Please check if it is the correct behavior.
Resizing...",
img.width(),
img.height(),
hint_width,
hint_height
);
img.resize_exact(hint_width, hint_height, FilterType::Triangle)
.to_luma8()
} else {
eprintln!(
"Image size matches hint size: {}x{}",
img.width(),
img.height()
);
img.to_luma8()
};
for x in 0..hint_width as usize {
for y in 0..hint_height as usize {
self.fractal_array[x << smooth][y << smooth] =
gray_hint_img.get_pixel(x as u32, y as u32)[0] as u32;
}
}
} else {
for x in 0..hint_width as usize {
for y in 0..hint_height as usize {
self.fractal_array[x << smooth][y << smooth] = random.random_range(0..256);
}
}
}
for pass in (0..smooth).rev() {
if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapY) {
for x in 0..=fractal_width as usize {
self.fractal_array[x][fractal_height as usize] = self.fractal_array[x][0];
}
} else if self.flags.contains(FractalFlags::Polar) {
for x in 0..=fractal_width as usize {
self.fractal_array[x][0] = 0;
self.fractal_array[x][fractal_height as usize] = 0;
}
}
if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapX) {
for y in 0..=fractal_height as usize {
self.fractal_array[fractal_width as usize][y] = self.fractal_array[0][y];
}
} else if self.flags.contains(FractalFlags::Polar) {
for y in 0..=fractal_height as usize {
self.fractal_array[0][y] = 0;
self.fractal_array[fractal_width as usize][y] = 0;
}
}
if self.flags.contains(FractalFlags::CenterRift) {
if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapY) {
for x in 0..=fractal_width as usize {
for y in 0..=(fractal_height / 6) as usize {
let factor =
((fractal_height / 12) as i32 - y as i32).unsigned_abs() + 1;
self.fractal_array[x][y] /= factor;
self.fractal_array[x][(fractal_height / 2) as usize + y] /= factor;
}
}
}
if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapX) {
for y in 0..=fractal_height as usize {
for x in 0..=(fractal_width / 6) as usize {
let factor =
((fractal_width / 12) as i32 - x as i32).unsigned_abs() + 1;
self.fractal_array[x][y] /= factor;
self.fractal_array[(fractal_width / 2) as usize + x][y] /= factor;
}
}
}
}
let screen = (1 << (pass + 1)) - 1;
for x in 0..((fractal_width >> pass) as usize
+ if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapX) {
0
} else {
1
})
{
for y in 0..((fractal_height >> pass) as usize
+ if self.fractal_grid.wrap_flags().contains(WrapFlags::WrapY) {
0
} else {
1
})
{
let mut sum = 0;
let randness = 1 << (7 - smooth + pass) as i32;
match ((x << pass) & screen != 0, (y << pass) & screen != 0) {
(true, true) => {
sum += self.fractal_array[(x - 1) << pass][(y - 1) << pass] as i32;
sum += self.fractal_array[(x + 1) << pass][(y - 1) << pass] as i32;
sum += self.fractal_array[(x - 1) << pass][(y + 1) << pass] as i32;
sum += self.fractal_array[(x + 1) << pass][(y + 1) << pass] as i32;
sum >>= 2;
sum += random.random_range(-randness..randness);
sum = sum.clamp(0, 255);
self.fractal_array[x << pass][y << pass] = sum as u32;
}
(true, false) => {
sum += self.fractal_array[(x - 1) << pass][y << pass] as i32;
sum += self.fractal_array[(x + 1) << pass][y << pass] as i32;
sum >>= 1;
sum += random.random_range(-randness..randness);
sum = sum.clamp(0, 255);
self.fractal_array[x << pass][y << pass] = sum as u32;
}
(false, true) => {
sum += self.fractal_array[x << pass][(y - 1) << pass] as i32;
sum += self.fractal_array[x << pass][(y + 1) << pass] as i32;
sum >>= 1;
sum += random.random_range(-randness..randness);
sum = sum.clamp(0, 255);
self.fractal_array[x << pass][y << pass] = sum as u32;
}
_ => {
}
}
}
}
}
if let Some(rifts) = rifts {
debug_assert!(
rifts.fractal_exp == self.fractal_exp,
"Rifts's dimension must be equal to the main fractal's dimension"
);
self.tectonic_action(rifts);
}
if self.flags.contains(FractalFlags::InvertHeights) {
self.fractal_array
.iter_mut()
.flatten()
.for_each(|val| *val = 255 - *val);
}
}
pub fn get_height(&self, x: i32, y: i32) -> u32 {
debug_assert!(
0 <= x && x < self.map_size.width as i32,
"'x' is out of the range of the grid width"
);
debug_assert!(
0 <= y && y < self.map_size.height as i32,
"'y' is out of the range of the grid height"
);
let fractal_exp = self.fractal_exp;
let fractal_width = fractal_exp.fractal_width();
let fractal_height = fractal_exp.fractal_height();
let width_ratio = fractal_width as f64 / self.map_size.width as f64;
let height_ratio = fractal_height as f64 / self.map_size.height as f64;
let src_x = (x as f64 + 0.5) * width_ratio - 0.5;
let src_y = (y as f64 + 0.5) * height_ratio - 0.5;
let x_diff = src_x - src_x.floor();
let y_diff = src_y - src_y.floor();
let src_x = min(src_x as usize, fractal_width as usize - 1);
let src_y = min(src_y as usize, fractal_height as usize - 1);
let value = (1.0 - x_diff) * (1.0 - y_diff) * self.fractal_array[src_x][src_y] as f64
+ x_diff * (1.0 - y_diff) * self.fractal_array[src_x + 1][src_y] as f64
+ (1.0 - x_diff) * y_diff * self.fractal_array[src_x][src_y + 1] as f64
+ x_diff * y_diff * self.fractal_array[src_x + 1][src_y + 1] as f64;
let height = value.clamp(0.0, 255.0) as u32;
if self.flags.contains(FractalFlags::Percent) {
(height * 100) >> 8
} else {
height
}
}
pub fn heights_from_percents<const N: usize>(&self, percents: [u32; N]) -> [u32; N] {
let percents = percents.map(|p| p.clamp(0, 100));
let mut flatten: Vec<&u32> = self
.fractal_array
.iter()
.take(self.fractal_array.len() - 1)
.flat_map(|row| row.iter().take(row.len() - 1))
.collect();
flatten.sort_unstable();
let len = flatten.len();
percents.map(|percent| {
let target_index = ((len - 1) * percent as usize) / 100;
let target_value = flatten[target_index];
*target_value
})
}
fn tectonic_action(&mut self, rifts: &CvFractal<G>) {
let fractal_exp = self.fractal_exp;
let fractal_width = fractal_exp.fractal_width();
let fractal_height = fractal_exp.fractal_height();
const DEEP: u32 = 0;
let rift_x = (fractal_width / 4) * 3;
const WIDTH: u32 = 16;
let rift_y = (fractal_height / 4) * 3;
const HEIGHT: u32 = 16;
if self.fractal_grid.wrap_x() {
for y in 0..=fractal_height as usize {
let rift_value = (rifts.fractal_array[rift_x as usize][y] as i32 - 128)
* fractal_width as i32
/ 128
/ 8;
for x in 0..WIDTH {
let right_x = (rift_value + x as i32).rem_euclid(fractal_width as i32) as usize;
let left_x = (rift_value - x as i32).rem_euclid(fractal_width as i32) as usize;
self.fractal_array[right_x][y] =
(self.fractal_array[right_x][y] * x + DEEP * (WIDTH - x)) / WIDTH;
self.fractal_array[left_x][y] =
(self.fractal_array[left_x][y] * x + DEEP * (WIDTH - x)) / WIDTH;
}
}
for y in 0..=fractal_height as usize {
self.fractal_array[fractal_width as usize][y] = self.fractal_array[0][y];
}
}
if self.fractal_grid.wrap_y() {
for x in 0..=fractal_width as usize {
let rift_value = (rifts.fractal_array[x][rift_y as usize] as i32 - 128)
* fractal_height as i32
/ 128
/ 8;
for y in 0..HEIGHT {
let top_y = (rift_value + y as i32).rem_euclid(fractal_height as i32) as usize;
let bottom_y =
(rift_value - y as i32).rem_euclid(fractal_height as i32) as usize;
self.fractal_array[x][top_y] =
(self.fractal_array[x][top_y] * y + DEEP * (HEIGHT - y)) / HEIGHT;
self.fractal_array[x][bottom_y] =
(self.fractal_array[x][bottom_y] * y + DEEP * (HEIGHT - y)) / HEIGHT;
}
}
for x in 0..=fractal_width as usize {
self.fractal_array[x][fractal_height as usize] = self.fractal_array[x][0];
}
}
}
pub fn ridge_builder(
&mut self,
random: &mut StdRng,
num_voronoi_seeds: u32,
ridge_flags: FractalFlags,
blend_ridge: u32,
blend_fract: u32,
) {
let fractal_exp = self.fractal_exp;
let fractal_width = fractal_exp.fractal_width();
let fractal_height = fractal_exp.fractal_height();
let num_voronoi_seeds = max(num_voronoi_seeds, 3);
let mut voronoi_seeds: Vec<VoronoiSeed> = Vec::with_capacity(num_voronoi_seeds as usize);
for _ in 0..num_voronoi_seeds {
let mut voronoi_seed;
loop {
voronoi_seed = VoronoiSeed::random_seed(random, &self.fractal_grid);
let is_too_close = voronoi_seeds.iter().any(|existing_seed| {
let distance_between_voronoi_seeds = self
.fractal_grid
.distance_to(voronoi_seed.cell, existing_seed.cell);
distance_between_voronoi_seeds < 7
});
if !is_too_close {
break;
}
}
voronoi_seeds.push(voronoi_seed);
}
for x in 0..fractal_width as usize {
for y in 0..fractal_height as usize {
let current_offset_coordinate = OffsetCoordinate::new(x as i32, y as i32);
let current_cell = self
.fractal_grid
.offset_to_cell(current_offset_coordinate)
.unwrap();
let mut closest_seed_distance = i32::MAX;
let mut next_closest_seed_distance = i32::MAX;
for current_voronoi_seed in &voronoi_seeds {
let mut modified_distance = self
.fractal_grid
.distance_to(current_cell, current_voronoi_seed.cell);
if !ridge_flags.is_empty() {
modified_distance += current_voronoi_seed.weakness as i32;
let relative_direction = self
.fractal_grid
.estimate_direction(current_cell, current_voronoi_seed.cell);
if relative_direction == Some(current_voronoi_seed.bias_direction) {
modified_distance -=
current_voronoi_seed.directional_bias_strength as i32;
} else if relative_direction
== Some(current_voronoi_seed.bias_direction.opposite())
{
modified_distance +=
current_voronoi_seed.directional_bias_strength as i32;
}
modified_distance = max(1, modified_distance);
}
if modified_distance < closest_seed_distance {
next_closest_seed_distance = closest_seed_distance;
closest_seed_distance = modified_distance;
} else if modified_distance < next_closest_seed_distance {
next_closest_seed_distance = modified_distance;
}
}
let ridge_height =
(255 * closest_seed_distance as u32) / next_closest_seed_distance as u32;
self.fractal_array[x][y] = (ridge_height * blend_ridge
+ self.fractal_array[x][y] * blend_fract)
/ max(blend_ridge + blend_fract, 1);
}
}
}
pub fn write_to_file(&self, path: &Path) {
let width = self.map_size.width;
let height = self.map_size.height;
let pixels: Vec<u8> = (0..height)
.flat_map(|y| (0..width).map(move |x| self.get_height(x as i32, y as i32) as u8))
.collect();
let _ = image::save_buffer(
Path::new(&path),
&pixels,
width,
height,
image::ColorType::L8,
);
}
pub fn write_to_file_by_image(&self, path: &Path) {
let map_width = self.map_size.width;
let map_height = self.map_size.height;
let fractal_exp = self.fractal_exp;
let fractal_width = fractal_exp.fractal_width();
let fractal_height = fractal_exp.fractal_height();
let pixels: Vec<u8> = (0..fractal_height as usize)
.flat_map(|y| (0..fractal_width as usize).map(move |x| self.fractal_array[x][y] as u8))
.collect();
let image: GrayImage =
ImageBuffer::from_raw(fractal_width, fractal_height, pixels).unwrap();
let resized_image = resize(&image, map_width, map_height, FilterType::Triangle);
resized_image.save(path).unwrap();
}
}
pub struct CvFractalBuilder<'a, G: Grid> {
grid: G,
grain: u32,
flags: FractalFlags,
hint_image: Option<&'a DynamicImage>,
rift_fractal: Option<&'a CvFractal<G>>,
fractal_exp: FractalExp,
}
impl<'a, G: Grid> CvFractalBuilder<'a, G> {
pub fn new(grid: G) -> Self {
Self {
grid,
grain: 2,
flags: FractalFlags::empty(),
hint_image: None,
rift_fractal: None,
fractal_exp: FractalExp::new(DEFAULT_WIDTH_EXP, DEFAULT_HEIGHT_EXP),
}
}
pub fn grain(mut self, grain: u32) -> Self {
self.grain = grain;
self
}
pub fn flags(mut self, flags: FractalFlags) -> Self {
self.flags = flags;
self
}
pub fn hint_image(mut self, hint_image: &'a DynamicImage) -> Self {
self.hint_image = Some(hint_image);
self
}
pub fn rift_fractal(mut self, rift_fractal: &'a CvFractal<G>) -> Self {
self.rift_fractal = Some(rift_fractal);
self
}
pub fn fractal_exp(mut self, fractal_exp: FractalExp) -> Self {
self.fractal_exp = fractal_exp;
self
}
pub fn build(self, random: &mut StdRng) -> CvFractal<G> {
let mut fractal = CvFractal::empty(self.grid, self.flags, self.fractal_exp);
let rifts = self.rift_fractal;
fractal.generate_fractal(random, self.grain, None, rifts);
fractal
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FractalFlags: u8 {
const Polar = 0b00000001;
const Percent = 0b00000010;
const CenterRift = 0b00000100;
const InvertHeights = 0b00001000;
}
}
struct VoronoiSeed {
pub cell: Cell,
pub weakness: u32,
pub bias_direction: Direction,
pub directional_bias_strength: u32,
}
impl VoronoiSeed {
pub fn random_seed(random: &mut StdRng, fractal_grid: &impl Grid) -> Self {
let offset_coordinate = OffsetCoordinate::from([
random.random_range(0..fractal_grid.width()),
random.random_range(0..fractal_grid.height()),
]);
let cell = fractal_grid.offset_to_cell(offset_coordinate).unwrap();
let weakness = random.random_range(0..6);
let bias_direction = *fractal_grid
.edge_direction_array()
.as_ref()
.choose(random)
.unwrap();
let directional_bias_strength = random.random_range(0..4);
VoronoiSeed {
cell,
weakness,
bias_direction,
directional_bias_strength,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FractalExp {
width_exp: u32,
height_exp: u32,
}
impl FractalExp {
pub const fn new(width_exp: u32, height_exp: u32) -> Self {
Self {
width_exp,
height_exp,
}
}
#[inline]
pub const fn width_exp(&self) -> u32 {
self.width_exp
}
#[inline]
pub const fn height_exp(&self) -> u32 {
self.height_exp
}
#[inline]
pub const fn fractal_width(&self) -> u32 {
1 << self.width_exp
}
#[inline]
pub const fn fractal_height(&self) -> u32 {
1 << self.height_exp
}
}