worlds-history-sim-rs/planet/src/world.rs
2022-09-20 15:14:16 +02:00

636 lines
21 KiB
Rust

// TODO: Logging doesn't seem to work here? Figure out why and fix
use {
crate::{
math_util::{
cartesian_coordinates,
mix_values,
random_point_in_sphere,
CartesianError,
RepeatNum,
},
perlin,
BiomeStats,
BiomeType,
},
bevy::{
log::info,
math::Vec3A,
prelude::Vec2,
utils::{default, HashMap},
},
rand::{rngs::StdRng, Rng, SeedableRng},
serde::{Deserialize, Serialize},
std::{
error::Error,
f32::consts::{PI, TAU},
fmt::{Debug, Display},
},
};
#[derive(Debug, Clone, Copy)]
pub enum WorldGenError {
CartesianError(CartesianError),
}
impl Error for WorldGenError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match *self {
WorldGenError::CartesianError(ref e) => Some(e),
}
}
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
fn cause(&self) -> Option<&dyn Error> {
self.source()
}
}
impl Display for WorldGenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
WorldGenError::CartesianError(err) => Display::fmt(err, f),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CompassDirection {
North,
NorthEast,
East,
SouthEast,
South,
SouthWest,
West,
NorthWest,
}
#[derive(Debug, Serialize)]
pub struct World {
pub width: u32,
pub height: u32,
pub seed: u32,
pub terrain: Vec<Vec<TerrainCell>>,
pub continent_offsets: [Vec2; World::NUM_CONTINENTS as usize],
pub continent_sizes: [Vec2; World::NUM_CONTINENTS as usize],
#[serde(skip)]
pub max_altitude: f32,
#[serde(skip)]
pub min_altitude: f32,
#[serde(skip)]
pub max_rainfall: f32,
#[serde(skip)]
pub min_rainfall: f32,
#[serde(skip)]
pub max_temperature: f32,
#[serde(skip)]
pub min_temperature: f32,
#[serde(skip)]
pub rng: StdRng,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct TerrainCell {
pub altitude: f32,
pub rainfall: f32,
pub temperature: f32,
pub biome_presences: Vec<(BiomeType, f32)>,
}
impl World {
const ALTITUDE_SPAN: f32 = World::MAX_ALTITUDE - World::MIN_ALTITUDE;
const CONTINENT_MAX_SIZE_FACTOR: f32 = 6.0;
const CONTINENT_MIN_SIZE_FACTOR: f32 = 2.5;
pub(crate) const MAX_ALTITUDE: f32 = 15000.0;
pub(crate) const MAX_RAINFALL: f32 = 7500.0;
pub(crate) const MAX_TEMPERATURE: f32 = 30.0;
pub(crate) const MIN_ALTITUDE: f32 = -15000.0;
pub(crate) const MIN_RAINFALL: f32 = 0.0;
pub(crate) const MIN_TEMPERATURE: f32 = -35.0;
const MOUNTAIN_RANGE_MIX_FACTOR: f32 = 0.075;
const MOUNTAIN_RANGE_WIDTH_FACTOR: f32 = 25.0;
const NUM_CONTINENTS: u8 = 7;
const RAINFALL_DRYNESS_FACTOR: f32 = 0.005;
const RAINFALL_DRYNESS_OFFSET: f32 = World::RAINFALL_DRYNESS_FACTOR * World::MAX_RAINFALL;
const RAINFALL_SPAN: f32 = World::MAX_RAINFALL - World::MIN_RAINFALL;
const TEMPERATURE_ALTITUDE_FACTOR: f32 = 1.0;
const TEMPERATURE_SPAN: f32 = World::MAX_TEMPERATURE - World::MIN_TEMPERATURE;
const TERRAIN_NOISE_FACTOR_1: f32 = 0.15;
const TERRAIN_NOISE_FACTOR_2: f32 = 0.15;
const TERRAIN_NOISE_FACTOR_3: f32 = 0.1;
const TERRAIN_NOISE_FACTOR_4: f32 = 2.5;
pub fn new(width: u32, height: u32, seed: u32) -> World {
World {
width,
height,
seed,
terrain: vec![
vec![TerrainCell::default(); width.try_into().unwrap()];
height.try_into().unwrap()
],
continent_offsets: [default(); World::NUM_CONTINENTS as usize],
continent_sizes: [default(); World::NUM_CONTINENTS as usize],
max_altitude: World::MIN_ALTITUDE,
min_altitude: World::MAX_ALTITUDE,
max_rainfall: World::MIN_RAINFALL,
min_rainfall: World::MAX_RAINFALL,
max_temperature: World::MIN_TEMPERATURE,
min_temperature: World::MAX_TEMPERATURE,
rng: StdRng::seed_from_u64(seed as u64),
}
}
pub fn generate(&mut self) -> Result<(), WorldGenError> {
if let Err(err) = self.generate_altitude() {
return Err(WorldGenError::CartesianError(err));
}
if let Err(err) = self.generate_rainfall() {
return Err(WorldGenError::CartesianError(err));
}
if let Err(err) = self.generate_temperature() {
return Err(WorldGenError::CartesianError(err));
}
self.generate_biomes();
Ok(())
}
fn generate_continents(&mut self) {
info!("Generating continents");
let width = self.width as f32;
let height = self.height as f32;
for i in 0..World::NUM_CONTINENTS {
info!("{}/{}", i, World::NUM_CONTINENTS);
self.continent_offsets[i as usize].x = self
.rng
.gen_range(width * i as f32 * 2.0 / 5.0..(width * (i as f32 + 2.0) * 2.0 / 5.0))
.repeat(width);
self.continent_offsets[i as usize].y =
self.rng.gen_range(height * 1.0 / 6.0..height * 5.0 / 6.0);
self.continent_sizes[i as usize] = Vec2 {
x: self
.rng
.gen_range(World::CONTINENT_MIN_SIZE_FACTOR..World::CONTINENT_MAX_SIZE_FACTOR),
y: self
.rng
.gen_range(World::CONTINENT_MIN_SIZE_FACTOR..World::CONTINENT_MAX_SIZE_FACTOR),
};
}
info!("Done generating continents");
}
fn continent_modifier(&self, x: usize, y: usize) -> f32 {
let x = x as f32;
let y = y as f32;
let width = self.width as f32;
let height = self.height as f32;
let mut max_value = 0.0;
let beta_factor = f32::sin(PI * y / height);
for i in 0..World::NUM_CONTINENTS {
let idx = i as usize;
let Vec2 {
x: offset_x,
y: offset_y,
} = self.continent_offsets[idx];
let Vec2 {
x: continent_width,
y: continent_height,
} = self.continent_sizes[idx];
let distance_x = f32::min(
f32::min(f32::abs(offset_x - x), f32::abs(width + offset_x - x)),
f32::abs(offset_x - x - width),
) * beta_factor
* continent_width;
let distance_y = f32::abs(offset_y - y) * continent_height;
let distance = f32::sqrt((distance_x * distance_x) + (distance_y * distance_y));
let value = f32::max(0.0, 1.0 - distance / width);
if value > max_value {
max_value = value;
}
}
max_value
}
fn generate_altitude(&mut self) -> Result<(), CartesianError> {
info!("Generating altitude");
self.generate_continents();
const RADIUS_1: f32 = 0.5;
const RADIUS_2: f32 = 4.0;
const RADIUS_3: f32 = 4.0;
const RADIUS_4: f32 = 8.0;
const RADIUS_5: f32 = 16.0;
let offset_1 = World::random_offset_vector(&mut self.rng);
let offset_2 = World::random_offset_vector(&mut self.rng);
let offset_3 = World::random_offset_vector(&mut self.rng);
let offset_4 = World::random_offset_vector(&mut self.rng);
let offset_5 = World::random_offset_vector(&mut self.rng);
for y in 0..self.terrain.len() {
let alpha = (y as f32 / self.height as f32) * PI;
for x in 0..self.terrain[y].len() {
let beta = (x as f32 / self.width as f32) * TAU;
let value_1 =
self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_1, offset_1)?;
let value_2 =
self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_2, offset_2)?;
let value_3 =
self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_3, offset_3)?;
let value_4 =
self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_4, offset_4)?;
let value_5 =
self.random_noise_from_polar_coordinates(alpha, beta, RADIUS_5, offset_5)?;
let value_a = mix_values(
self.continent_modifier(x, y),
value_3,
World::TERRAIN_NOISE_FACTOR_1,
) * mix_values(1.0, value_4, World::TERRAIN_NOISE_FACTOR_2)
* mix_values(1.0, value_5, World::TERRAIN_NOISE_FACTOR_3);
let value_b = value_a * 0.04 + 0.48;
let value_c = mix_values(
self.mountain_range_noise_from_random_noise(mix_values(
value_1,
value_2,
World::MOUNTAIN_RANGE_MIX_FACTOR,
)),
value_3,
World::TERRAIN_NOISE_FACTOR_1 * World::TERRAIN_NOISE_FACTOR_4,
) * mix_values(
1.0,
value_4,
World::TERRAIN_NOISE_FACTOR_2 * World::TERRAIN_NOISE_FACTOR_4,
) * mix_values(
1.0,
value_5,
World::TERRAIN_NOISE_FACTOR_3 * World::TERRAIN_NOISE_FACTOR_4,
);
let mut value_d = mix_values(value_a, value_c, 0.25);
value_d = mix_values(value_d, value_c, 0.1);
value_d = mix_values(value_d, value_b, 0.1);
let altitude = World::calculate_altitude(value_d);
self.terrain[y][x].altitude = altitude;
if altitude > self.max_altitude {
self.max_altitude = altitude;
}
if altitude < self.min_altitude {
self.min_altitude = altitude;
}
}
}
info!("Done generating altitude");
Ok(())
}
fn random_offset_vector(rng: &mut StdRng) -> Vec3A {
random_point_in_sphere(rng, 1000.0)
}
fn random_noise_from_polar_coordinates(
&self,
alpha: f32,
beta: f32,
radius: f32,
offset: Vec3A,
) -> Result<f32, CartesianError> {
let cartesian = cartesian_coordinates(alpha, beta, radius)?;
Ok(perlin::get_value(
cartesian.x + offset.x,
cartesian.y + offset.y,
cartesian.z + offset.z,
))
}
fn mountain_range_noise_from_random_noise(&self, noise: f32) -> f32 {
let noise = noise * 2.0 - 1.0;
let value_1 = -f32::exp(-f32::powi(
noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR + 1.0,
2,
));
let value_2 = f32::exp(-f32::powi(
noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR - 1.0,
2,
));
let value_3 = -f32::exp(-f32::powi(
noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR + World::MOUNTAIN_RANGE_WIDTH_FACTOR / 2.0,
2,
));
let value_4 = f32::exp(-f32::powi(
noise * World::MOUNTAIN_RANGE_WIDTH_FACTOR - World::MOUNTAIN_RANGE_WIDTH_FACTOR / 2.0,
2,
));
(value_1 + value_2 + value_3 + value_4 + 1.0) / 2.0
}
fn calculate_altitude(raw_altitude: f32) -> f32 {
World::MIN_ALTITUDE + (raw_altitude * World::ALTITUDE_SPAN)
}
fn generate_rainfall(&mut self) -> Result<(), CartesianError> {
info!("Generating rainfall");
const RADIUS: f32 = 2.0;
let offset = World::random_offset_vector(&mut self.rng);
let height = self.terrain.len();
for y in 0..height {
info!("Rainfall: {}/{}", y, height);
let alpha = (y as f32 / self.height as f32) * PI;
let width = self.terrain[y].len();
for x in 0..width {
let beta = (x as f32 / self.width as f32) * TAU;
let random_noise =
self.random_noise_from_polar_coordinates(alpha, beta, RADIUS, offset)?;
let latitude_factor = (alpha * 0.9) + (random_noise * 0.2 * PI) - 0.1;
let latitude_modifier_1 = (1.5 * f32::sin(latitude_factor)) - 0.5;
let latitude_modifier_2 = f32::cos(latitude_factor);
let offset_cell_x =
(width + x + f32::floor(latitude_modifier_2 * width as f32 / 20.0) as usize)
% width;
let offset_cell_2_x =
(width + x + f32::floor(latitude_modifier_2 * width as f32 / 10.0) as usize)
% width;
let offset_cell = &self.terrain[y][offset_cell_x];
let offset_altitude = f32::max(0.0, offset_cell.altitude);
let offset_cell_2 = &self.terrain[y][offset_cell_2_x];
let offset_altitude_2 = f32::max(0.0, offset_cell_2.altitude);
let cell = &mut self.terrain[y][x];
let altitude = f32::max(0.0, cell.altitude);
let altitude_modifier =
(altitude - (offset_altitude * 1.5) - (offset_altitude_2 * 1.5))
/ World::MAX_ALTITUDE;
let rainfall_value = mix_values(latitude_modifier_1, altitude_modifier, 0.63);
let rainfall = f32::min(
World::MAX_RAINFALL,
World::calculate_rainfall(rainfall_value),
);
cell.rainfall = rainfall;
if rainfall > self.max_rainfall {
self.max_rainfall = rainfall;
}
if rainfall < self.min_rainfall {
self.min_rainfall = rainfall;
}
}
}
info!("Done generating rainfall");
Ok(())
}
fn calculate_rainfall(raw_rainfall: f32) -> f32 {
f32::clamp(
(raw_rainfall * (World::RAINFALL_SPAN + World::RAINFALL_DRYNESS_OFFSET))
+ World::MIN_RAINFALL
- World::RAINFALL_DRYNESS_OFFSET,
0.0,
World::MAX_RAINFALL,
)
}
fn generate_temperature(&mut self) -> Result<(), CartesianError> {
let offset = World::random_offset_vector(&mut self.rng);
const RADIUS: f32 = 2.0;
for y in 0..self.terrain.len() {
let alpha = (y as f32 / self.height as f32) * PI;
for x in 0..self.terrain[y].len() {
let beta = (x as f32 / self.width as f32) * TAU;
let random_noise =
self.random_noise_from_polar_coordinates(alpha, beta, RADIUS, offset)?;
let cell = &mut self.terrain[y][x];
let latitude_modifer = mix_values(alpha, random_noise * PI, 0.1);
let altitude_factor = f32::max(
0.0,
(cell.altitude / World::MAX_ALTITUDE) * World::TEMPERATURE_ALTITUDE_FACTOR,
);
let temperature =
World::calculate_temperature(f32::sin(latitude_modifer) - altitude_factor);
cell.temperature = temperature;
if temperature > self.max_temperature {
self.max_temperature = temperature;
}
if temperature < self.min_temperature {
self.min_temperature = temperature;
}
}
}
Ok(())
}
fn calculate_temperature(raw_temperature: f32) -> f32 {
f32::clamp(
(raw_temperature * World::TEMPERATURE_SPAN) + World::MIN_TEMPERATURE,
World::MIN_TEMPERATURE,
World::MAX_TEMPERATURE,
)
}
fn generate_biomes(&mut self) {
for y in 0..self.terrain.len() {
for x in 0..self.terrain[y].len() {
let cell = &self.terrain[y][x];
let mut total_presence = 0.0;
let mut biome_presences = vec![];
for biome_type in BiomeType::iterator() {
let presence = self.biome_presence(cell, &biome_type.into());
if presence <= 0.0 {
continue;
}
biome_presences.push((*biome_type, presence));
total_presence += presence;
}
self.terrain[y][x].biome_presences = biome_presences
.iter()
.map(|(biome_type, presence)| (*biome_type, presence / total_presence))
.collect();
}
}
}
fn biome_presence(&self, cell: &TerrainCell, biome: &BiomeStats) -> f32 {
let mut presence = 0.0;
let altitude_diff = cell.altitude - biome.min_altitude;
if altitude_diff < 0.0 {
return 0.0;
}
let altitude_factor = altitude_diff / (biome.max_altitude - biome.min_altitude);
if altitude_factor > 1.0 {
return 0.0;
};
presence += if altitude_factor > 0.5 {
1.0 - altitude_factor
} else {
altitude_factor
};
let rainfall_diff = cell.rainfall - biome.min_rainfall;
if rainfall_diff < 0.0 {
return 0.0;
}
let rainfall_factor = rainfall_diff / (biome.max_rainfall - biome.min_rainfall);
if rainfall_factor > 1.0 {
return 0.0;
}
presence += if rainfall_factor > 0.5 {
1.0 - rainfall_factor
} else {
rainfall_factor
};
let temperature_diff = cell.temperature - biome.min_temperature;
if temperature_diff < 0.0 {
return 0.0;
}
let temperature_factor = temperature_diff / (biome.max_temperature - biome.min_temperature);
if temperature_factor > 1.0 {
return 0.0;
}
presence += if temperature_factor > 0.5 {
1.0 - temperature_factor
} else {
temperature_factor
};
presence
}
#[must_use]
pub fn cell_neighbors(&self, x: usize, y: usize) -> HashMap<CompassDirection, (usize, usize)> {
let mut neighbors = HashMap::new();
let height = self.height as usize;
let width = self.width as usize;
let north_edge = y >= height - 1;
let east_edge = x >= width - 1;
let south_edge = y == 0;
let west_edge = x == 0;
if !north_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::North, (y + 1, x));
}
if !north_edge && !east_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::NorthEast, (y + 1, x + 1));
}
if !east_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::East, (y, x + 1));
}
if !south_edge && !east_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::SouthEast, (y - 1, x + 1));
}
if !south_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::South, (y - 1, x));
}
if !south_edge && !west_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::SouthWest, (y - 1, x - 1));
}
if !west_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::West, (y, x - 1));
};
if !north_edge && !west_edge {
_ = neighbors.insert_unique_unchecked(CompassDirection::NorthWest, (y + 1, x - 1));
};
neighbors
}
#[must_use]
pub fn get_slant(&self, x: usize, y: usize) -> f32 {
let neighbors = self.cell_neighbors(x, y);
let terrain = &self.terrain;
let mut west_altitude = f32::MIN;
if let Some(neighbor_coords) = neighbors.get(&CompassDirection::North) {
west_altitude = f32::max(
west_altitude,
terrain[neighbor_coords.0][neighbor_coords.1].altitude,
);
}
if let Some(neighbor_coords) = neighbors.get(&CompassDirection::NorthWest) {
west_altitude = f32::max(
west_altitude,
terrain[neighbor_coords.0][neighbor_coords.1].altitude,
);
}
if let Some(neighbor_coords) = neighbors.get(&CompassDirection::West) {
west_altitude = f32::max(
west_altitude,
terrain[neighbor_coords.0][neighbor_coords.1].altitude,
);
}
let mut east_altitude = f32::MIN;
if let Some(neighbor_coords) = neighbors.get(&CompassDirection::North) {
east_altitude = f32::max(
east_altitude,
terrain[neighbor_coords.0][neighbor_coords.1].altitude,
);
}
if let Some(neighbor_coords) = neighbors.get(&CompassDirection::NorthWest) {
east_altitude = f32::max(
east_altitude,
terrain[neighbor_coords.0][neighbor_coords.1].altitude,
);
}
if let Some(neighbor_coords) = neighbors.get(&CompassDirection::West) {
east_altitude = f32::max(
east_altitude,
terrain[neighbor_coords.0][neighbor_coords.1].altitude,
);
}
west_altitude - east_altitude
}
}