Tweak world generation
055861b4e9fc202c10f02fa841f69dedbbe00e75
This commit is contained in:
parent
c1994382c1
commit
99ec0c33cc
4 changed files with 186 additions and 74 deletions
|
@ -69,3 +69,7 @@ pub fn random_point_in_sphere(radius: f32) -> Vec3A {
|
||||||
|
|
||||||
Vec3A::new(mult * x, mult * y, mult * z)
|
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))
|
||||||
|
}
|
||||||
|
|
|
@ -4,9 +4,11 @@ use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use bevy::{math::Vec3A, prelude::Vec2, utils::default};
|
||||||
use noise::{NoiseFn, Perlin, Seedable};
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum WorldGenError {
|
pub enum WorldGenError {
|
||||||
|
@ -35,13 +37,15 @@ impl Display for WorldGenError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct World {
|
pub struct World {
|
||||||
pub width: i32,
|
pub width: i32,
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
pub seed: u32,
|
pub seed: u32,
|
||||||
|
|
||||||
pub terrain: Vec<Vec<TerrainCell>>,
|
pub terrain: Vec<Vec<TerrainCell>>,
|
||||||
|
contintent_offsets: [Vec2; World::NUM_CONTINENTS as usize],
|
||||||
|
perlin: Perlin,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Default)]
|
#[derive(Debug, Copy, Clone, Default)]
|
||||||
|
@ -52,63 +56,85 @@ pub struct TerrainCell {
|
||||||
|
|
||||||
impl World {
|
impl World {
|
||||||
pub fn new(width: i32, height: i32, seed: u32) -> 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 {
|
World {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
seed,
|
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 MIN_ALTITUDE: f32 = -10000.0;
|
||||||
pub const MAX_ALTITUDE: f32 = 10000.0;
|
pub const MAX_ALTITUDE: f32 = 10000.0;
|
||||||
pub const ALTITUDE_SPAN: f32 = Self::MAX_ALTITUDE - Self::MIN_ALTITUDE;
|
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 MIN_RAINFALL: f32 = -10.0;
|
||||||
pub const MAX_RAINFALL: f32 = 100.0;
|
pub const MAX_RAINFALL: f32 = 100.0;
|
||||||
pub const RAINFALL_SPAN: f32 = Self::MAX_RAINFALL - Self::MIN_RAINFALL;
|
pub const RAINFALL_SPAN: f32 = Self::MAX_RAINFALL - Self::MIN_RAINFALL;
|
||||||
pub const RAINFALL_ALTITUDE_FACTOR: f32 = 1.0;
|
pub const RAINFALL_ALTITUDE_FACTOR: f32 = 1.0;
|
||||||
|
|
||||||
pub fn generate(&mut self) -> Result<(), WorldGenError> {
|
pub fn generate(&mut self) -> Result<(), WorldGenError> {
|
||||||
let perlin = Perlin::new().set_seed(self.seed);
|
if let Err(err) = self.generate_altitude() {
|
||||||
|
|
||||||
if let Err(err) = self.generate_altitude(&perlin) {
|
|
||||||
return Err(WorldGenError::CartesianError(err));
|
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));
|
return Err(WorldGenError::CartesianError(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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;
|
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 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_rainfall(&mut self, perlin: &Perlin) -> Result<(), CartesianError> {
|
fn calculate_altitude(raw_altitude: f32) -> f32 {
|
||||||
let offset = random_point_in_sphere(1000.0);
|
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;
|
const RADIUS: f32 = 2.0;
|
||||||
|
|
||||||
for (y, row) in self.terrain.iter_mut().enumerate() {
|
for (y, row) in self.terrain.iter_mut().enumerate() {
|
||||||
|
@ -118,7 +144,7 @@ impl World {
|
||||||
|
|
||||||
let pos = cartesian_coordinates(alpha, beta, RADIUS)? + offset;
|
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)
|
let base_rainfall = (value * Self::RAINFALL_SPAN + Self::MIN_RAINFALL)
|
||||||
.clamp(0.0, World::MAX_RAINFALL);
|
.clamp(0.0, World::MAX_RAINFALL);
|
||||||
|
@ -132,4 +158,69 @@ impl World {
|
||||||
}
|
}
|
||||||
Ok(())
|
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<f32, CartesianError> {
|
||||||
|
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<f32, CartesianError> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::{World, WorldGenError};
|
use crate::{TerrainCell, World, WorldGenError};
|
||||||
|
use bevy::render::color::Color;
|
||||||
use rand::random;
|
use rand::random;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -13,9 +14,72 @@ impl WorldManager {
|
||||||
self.world.as_ref()
|
self.world.as_ref()
|
||||||
}
|
}
|
||||||
pub fn new_world(&mut self) -> Result<&World, WorldGenError> {
|
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()?;
|
new_world.generate()?;
|
||||||
self.world = Some(new_world);
|
self.world = Some(new_world);
|
||||||
Ok(self.get_world().unwrap())
|
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<Color> {
|
||||||
|
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<u8> {
|
||||||
|
self.world_colors()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|color| {
|
||||||
|
color
|
||||||
|
.as_rgba_f32()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|num| num.to_le_bytes())
|
||||||
|
.collect::<Vec<u8>>()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
49
src/main.rs
49
src/main.rs
|
@ -60,62 +60,15 @@ use bevy::{
|
||||||
};
|
};
|
||||||
use save::*;
|
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(
|
fn generate_texture(
|
||||||
mut commands: Commands<'_, '_>,
|
mut commands: Commands<'_, '_>,
|
||||||
mut images: ResMut<'_, Assets<Image>>,
|
mut images: ResMut<'_, Assets<Image>>,
|
||||||
world_manager: Res<'_, WorldManager>,
|
world_manager: Res<'_, WorldManager>,
|
||||||
) {
|
) {
|
||||||
let world = world_manager.get_world().unwrap();
|
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::<Vec<u8>>()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let image_handle = images.add(Image {
|
let image_handle = images.add(Image {
|
||||||
data,
|
data: world_manager.world_color_bytes(),
|
||||||
texture_descriptor: TextureDescriptor {
|
texture_descriptor: TextureDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
size: Extent3d {
|
size: Extent3d {
|
||||||
|
|
Loading…
Reference in a new issue