diff --git a/save/src/math_util.rs b/save/src/math_util.rs index 817b532..595e527 100644 --- a/save/src/math_util.rs +++ b/save/src/math_util.rs @@ -69,3 +69,7 @@ pub fn random_point_in_sphere(radius: f32) -> Vec3A { Vec3A::new(mult * x, mult * y, mult * z) } + +pub fn mix_values(a: f32, b: f32, weight_b: f32) -> f32 { + (b * weight_b) + (a * (1.0 - weight_b)) +} diff --git a/save/src/world.rs b/save/src/world.rs index 989957a..0e77452 100644 --- a/save/src/world.rs +++ b/save/src/world.rs @@ -4,9 +4,11 @@ use std::{ fmt::Display, }; +use bevy::{math::Vec3A, prelude::Vec2, utils::default}; use noise::{NoiseFn, Perlin, Seedable}; +use rand::Rng; -use crate::{cartesian_coordinates, random_point_in_sphere, CartesianError}; +use crate::{cartesian_coordinates, mix_values, random_point_in_sphere, CartesianError}; #[derive(Debug, Clone, Copy)] pub enum WorldGenError { @@ -35,13 +37,15 @@ impl Display for WorldGenError { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct World { pub width: i32, pub height: i32, pub seed: u32, pub terrain: Vec>, + contintent_offsets: [Vec2; World::NUM_CONTINENTS as usize], + perlin: Perlin, } #[derive(Debug, Copy, Clone, Default)] @@ -52,63 +56,85 @@ pub struct TerrainCell { impl World { pub fn new(width: i32, height: i32, seed: u32) -> World { - let terrain = vec![ - vec![TerrainCell::default(); width.try_into().unwrap()]; - height.try_into().unwrap() - ]; - World { width, height, seed, - terrain, + terrain: vec![ + vec![TerrainCell::default(); width.try_into().unwrap()]; + height.try_into().unwrap() + ], + contintent_offsets: [default(); Self::NUM_CONTINENTS as usize], + perlin: Perlin::new().set_seed(seed), } } + pub const NUM_CONTINENTS: u8 = 3; + pub const CONTINTENT_FACTOR: f32 = 0.5; + pub const MIN_ALTITUDE: f32 = -10000.0; pub const MAX_ALTITUDE: f32 = 10000.0; pub const ALTITUDE_SPAN: f32 = Self::MAX_ALTITUDE - Self::MIN_ALTITUDE; + pub const MOUNTAIN_RANGE_FACTOR: f32 = 0.1; + pub const MOUNTAIN_RANGE_WIDTH_FACTOR: f32 = 10.0; + pub const MIN_RAINFALL: f32 = -10.0; pub const MAX_RAINFALL: f32 = 100.0; pub const RAINFALL_SPAN: f32 = Self::MAX_RAINFALL - Self::MIN_RAINFALL; pub const RAINFALL_ALTITUDE_FACTOR: f32 = 1.0; pub fn generate(&mut self) -> Result<(), WorldGenError> { - let perlin = Perlin::new().set_seed(self.seed); - - if let Err(err) = self.generate_altitude(&perlin) { + if let Err(err) = self.generate_altitude() { return Err(WorldGenError::CartesianError(err)); } - if let Err(err) = self.generate_rainfall(&perlin) { + if let Err(err) = self.generate_rainfall() { return Err(WorldGenError::CartesianError(err)); } Ok(()) } - fn generate_altitude(&mut self, perlin: &Perlin) -> Result<(), CartesianError> { - let offset = random_point_in_sphere(1000.0); - const RADIUS: f32 = 2.0; - for (y, row) in self.terrain.iter_mut().enumerate() { + fn generate_altitude(&mut self) -> Result<(), CartesianError> { + self.generate_continents(); + + let offset_1 = Self::random_offset_vector(); + const RADIUS_1: f32 = 2.0; + + let offset_2 = Self::random_offset_vector(); + const RADIUS_2: f32 = 1.0; + + for y in 0..self.terrain.len() { let alpha = (y as f32 / self.height as f32) * PI; - for (x, cell) in row.iter_mut().enumerate() { + for x in 0..self.terrain[y].len() { let beta = (x as f32 / self.width as f32) * TAU; - let pos = cartesian_coordinates(alpha, beta, RADIUS)? + offset; - let value = Perlin::get(perlin, [pos.x.into(), pos.y.into(), pos.z.into()]) as f32; + let value_1 = + self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_1, offset_1)?; + let value_2 = self.random_mountain_noise_from_polar_coordinates( + alpha, beta, RADIUS_2, offset_2, + )?; - let altitude = Self::MIN_ALTITUDE + (value * Self::ALTITUDE_SPAN); + let raw_altitude = mix_values(value_1, value_2, Self::MOUNTAIN_RANGE_FACTOR); + let raw_altitude = mix_values( + raw_altitude, + self.get_continent_modifier(x, y), + Self::CONTINTENT_FACTOR, + ); - cell.altitude = altitude; + self.terrain[y][x].altitude = Self::calculate_altitude(raw_altitude); } } Ok(()) } - fn generate_rainfall(&mut self, perlin: &Perlin) -> Result<(), CartesianError> { - let offset = random_point_in_sphere(1000.0); + fn calculate_altitude(raw_altitude: f32) -> f32 { + Self::MIN_ALTITUDE + (raw_altitude * Self::ALTITUDE_SPAN) + } + + fn generate_rainfall(&mut self) -> Result<(), CartesianError> { + let offset = Self::random_offset_vector(); const RADIUS: f32 = 2.0; for (y, row) in self.terrain.iter_mut().enumerate() { @@ -118,7 +144,7 @@ impl World { let pos = cartesian_coordinates(alpha, beta, RADIUS)? + offset; - let value = Perlin::get(perlin, [pos.x.into(), pos.y.into(), pos.z.into()]) as f32; + let value = self.perlin.get([pos.x.into(), pos.y.into(), pos.z.into()]) as f32; let base_rainfall = (value * Self::RAINFALL_SPAN + Self::MIN_RAINFALL) .clamp(0.0, World::MAX_RAINFALL); @@ -132,4 +158,69 @@ impl World { } Ok(()) } + + fn generate_continents(&mut self) { + let mut rng = rand::thread_rng(); + self.contintent_offsets.fill_with(|| Vec2 { + x: rng.gen_range(1.0..(self.width - 1) as f32), + y: rng.gen_range(1.0..(self.width - 1) as f32), + }); + } + + fn get_continent_modifier(&self, x: usize, y: usize) -> f32 { + let mut max_value = 0.0; + + for Vec2 { + x: cont_x, + y: cont_y, + } in self.contintent_offsets + { + let distance_x = f32::min( + f32::abs(cont_x - x as f32), + f32::abs(self.width as f32 + cont_x - x as f32), + ); + let distance_y = f32::abs(cont_y - y as f32); + + let factor_x = f32::max(0.0, 1.0 - distance_x / self.width as f32); + let factor_y = f32::max(0.0, 1.0 - distance_y / self.height as f32); + + max_value = f32::max(max_value, factor_x * factor_x * factor_y * factor_y); + } + + max_value + } + + fn random_offset_vector() -> Vec3A { + random_point_in_sphere(1000.0) + } + + fn random_mountain_noise_from_polar_coordinates( + &self, + alpha: f32, + beta: f32, + radius: f32, + offset: Vec3A, + ) -> Result { + let noise = World::random_noise_from_polar_coordinates(self, alpha, beta, radius, offset)? + * 2.0 + - 1.0; + + let value_1 = (-(noise * Self::MOUNTAIN_RANGE_WIDTH_FACTOR + 1.0).powf(2.0)).exp(); + let value_2 = -(-(noise * Self::MOUNTAIN_RANGE_WIDTH_FACTOR - 1.0).powf(2.0)).exp(); + + Ok((value_1 + value_2 + 1.0) / 2.0) + } + + fn random_noise_from_polar_coordinates( + &self, + alpha: f32, + beta: f32, + radius: f32, + offset: Vec3A, + ) -> Result { + let offset = cartesian_coordinates(alpha, beta, radius)? + offset; + Ok(self + .perlin + .get([offset.x as f64, offset.y as f64, offset.z as f64]) as f32) + } } diff --git a/save/src/world_manager.rs b/save/src/world_manager.rs index dd60f60..36c76dd 100644 --- a/save/src/world_manager.rs +++ b/save/src/world_manager.rs @@ -1,4 +1,5 @@ -use crate::{World, WorldGenError}; +use crate::{TerrainCell, World, WorldGenError}; +use bevy::render::color::Color; use rand::random; #[derive(Debug)] @@ -13,9 +14,72 @@ impl WorldManager { self.world.as_ref() } pub fn new_world(&mut self) -> Result<&World, WorldGenError> { - let mut new_world = World::new(800, 600, random()); + let seed = random(); + let mut new_world = World::new(800, 600, seed); new_world.generate()?; self.world = Some(new_world); Ok(self.get_world().unwrap()) } + + fn generate_color(cell: &TerrainCell) -> Color { + let altitude_color = Self::altitude_color(cell.altitude); + let rainfall_color = Self::rainfall_color(cell.rainfall); + + let normalized_rainfall = f32::max(cell.rainfall / World::MAX_RAINFALL, 0.0); + + let r = (altitude_color.r() * (1.0 - normalized_rainfall)) + + (rainfall_color.r() * normalized_rainfall); + let g = (altitude_color.g() * (1.0 - normalized_rainfall)) + + (rainfall_color.g() * normalized_rainfall); + let b = (altitude_color.b() * (1.0 - normalized_rainfall)) + + (rainfall_color.b() * normalized_rainfall); + + Color::rgb(r, g, b) + } + + fn altitude_color(altitude: f32) -> Color { + if altitude < 0.0 { + Color::BLUE + } else { + let mult = (altitude - World::MIN_ALTITUDE) / World::MAX_ALTITUDE; + + Color::rgb(0.58 * mult, 0.29 * mult, 0.0) + } + } + + fn rainfall_color(rainfall: f32) -> Color { + if rainfall < 0.0 { + Color::BLACK + } else { + let mult = rainfall / World::MAX_RAINFALL; + Color::GREEN * mult + } + } + + pub fn world_colors(&self) -> Vec { + match self.get_world() { + None => panic!("Called world_colors before generating world"), + Some(world) => { + let terrain_cells: Vec<_> = world.terrain.iter().rev().flatten().collect(); + + terrain_cells + .iter() + .map(|cell| Self::generate_color(cell)) + .collect() + } + } + } + + pub fn world_color_bytes(&self) -> Vec { + self.world_colors() + .iter() + .flat_map(|color| { + color + .as_rgba_f32() + .iter() + .flat_map(|num| num.to_le_bytes()) + .collect::>() + }) + .collect() + } } diff --git a/src/main.rs b/src/main.rs index c6dfa43..f72e29f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,62 +60,15 @@ use bevy::{ }; use save::*; -fn get_color(cell: &TerrainCell) -> Color { - let altitude_color = gen_altitude_color(cell.altitude); - let rainfall_color = gen_rainfall_color(cell.rainfall); - - let normalized_rainfall = f32::max(cell.rainfall / World::MAX_RAINFALL, 0.0); - - let red = (altitude_color.r() * (1.0 - normalized_rainfall)) - + rainfall_color.r() * normalized_rainfall; - let green = (altitude_color.g() * (1.0 - normalized_rainfall)) - + rainfall_color.g() * normalized_rainfall; - let blue = (altitude_color.b() * (1.0 - normalized_rainfall)) - + rainfall_color.b() * normalized_rainfall; - - Color::rgb(red, green, blue) -} - -fn gen_altitude_color(altitude: f32) -> Color { - if altitude < 0.0 { - Color::BLUE - } else { - let mult = (altitude - World::MIN_ALTITUDE) / World::ALTITUDE_SPAN; - - Color::rgb(0.58 * mult, 0.29 * mult, 0.0) - } -} - -fn gen_rainfall_color(rainfall: f32) -> Color { - if rainfall < 0.0 { - Color::BLACK - } else { - let mult = rainfall / World::MAX_RAINFALL; - Color::GREEN * mult - } -} - fn generate_texture( mut commands: Commands<'_, '_>, mut images: ResMut<'_, Assets>, world_manager: Res<'_, WorldManager>, ) { let world = world_manager.get_world().unwrap(); - let terrain_cells: Vec<_> = world.terrain.iter().rev().flatten().collect(); - let colors: Vec<_> = terrain_cells.iter().map(|cell| get_color(cell)).collect(); - let data: Vec<_> = colors - .iter() - .flat_map(|color| { - color - .as_rgba_f32() - .iter() - .flat_map(|num| num.to_le_bytes()) - .collect::>() - }) - .collect(); let image_handle = images.add(Image { - data, + data: world_manager.world_color_bytes(), texture_descriptor: TextureDescriptor { label: None, size: Extent3d {