Merge branch 'zesterer/worldgen' into 'master'

Worldgen Improvements

See merge request veloren/veloren!1768
This commit is contained in:
Marcel 2021-03-07 18:01:00 +00:00
commit 52cfc2f301
68 changed files with 3287 additions and 314 deletions

View File

@ -41,6 +41,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New enemies in 5 lower dungeons
- Added on join event in plugins
- Item stacking and splitting
- Procedural trees (currently only oaks and pines are procedural)
- Cliffs on steep slopes
- Giant tree sites
### Changed
@ -63,6 +66,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Items can be requested from the counterparty's inventory during trade.
- Savanna grasses restricted to savanna, cacti to desert.
- Fireworks recursively shoot more fireworks.
- Improved static light rendering and illumination
- Improved the tree spawning model to allow for overlapping forests
- Changed sunlight (and, in general, static light) propagation through blocks to allow for more material properties
### Removed

3
Cargo.lock generated
View File

@ -5884,6 +5884,7 @@ dependencies = [
"bincode",
"bitvec",
"criterion",
"enum-iterator",
"fxhash",
"hashbrown 0.9.1",
"image",
@ -5899,6 +5900,8 @@ dependencies = [
"rayon",
"ron",
"serde",
"structopt",
"svg_fmt",
"tracing",
"tracing-subscriber",
"vek 0.14.1",

BIN
assets/voxygen/element/map/excl.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/map/tree.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/map/tree_hover.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -9,9 +9,11 @@
"hud.map.difficulty": "Difficulty",
"hud.map.towns": "Towns",
"hud.map.castles": "Castles",
"hud.map.dungeons": "Dungeons",
"hud.map.dungeons": "Dungeons",
"hud.map.caves": "Caves",
"hud.map.cave": "Cave",
"hud.map.trees": "Giant Trees",
"hud.map.tree": "Giant Tree",
"hud.map.town": "Town",
"hud.map.castle": "Castle",
"hud.map.dungeon": "Dungeon",

View File

@ -1,5 +1,7 @@
#version 330 core
#define FIGURE_SHADER
#include <constants.glsl>
#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION
@ -50,6 +52,7 @@ uniform u_locals {
mat4 model_mat;
vec4 highlight_col;
vec4 model_light;
vec4 model_glow;
ivec4 atlas_offs;
vec3 model_pos;
// bit 0 - is player
@ -181,7 +184,10 @@ void main() {
float ao = f_ao * sqrt(f_ao);//0.25 + f_ao * 0.75; ///*pow(f_ao, 0.5)*/f_ao * 0.85 + 0.15;
vec3 glow = pow(model_light.y, 3) * 4 * GLOW_COLOR;
float glow_mag = length(model_glow.xyz);
vec3 glow = pow(model_glow.w, 2) * 4
* glow_light(f_pos)
* (max(dot(f_norm, model_glow.xyz / glow_mag) * 0.5 + 0.5, 0.0) + max(1.0 - glow_mag, 0.0));
emitted_light += glow;
reflected_light *= ao;

View File

@ -2,6 +2,8 @@
#include <constants.glsl>
#define FIGURE_SHADER
#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION
#define LIGHTING_REFLECTION_KIND LIGHTING_REFLECTION_KIND_GLOSSY
@ -28,6 +30,7 @@ uniform u_locals {
mat4 model_mat;
vec4 highlight_col;
vec4 model_light;
vec4 model_glow;
ivec4 atlas_offs;
vec3 model_pos;
// bit 0 - is player

View File

@ -139,6 +139,10 @@ float lights_at(vec3 wpos, vec3 wnorm, vec3 /*cam_to_frag*/view_dir, vec3 mu, ve
vec3 difference = light_pos - wpos;
float distance_2 = dot(difference, difference);
if (distance_2 > 10000.0) {
continue;
}
// float strength = attenuation_strength(difference);// pow(attenuation_strength(difference), 0.6);
// NOTE: This normalizes strength to 0.25 at the center of the point source.
float strength = 1.0 / (4 + distance_2);
@ -183,7 +187,12 @@ float lights_at(vec3 wpos, vec3 wnorm, vec3 /*cam_to_frag*/view_dir, vec3 mu, ve
vec3 direct_light = PI * color * strength * square_factor * light_reflection_factor(/*direct_norm_dir*/wnorm, /*cam_to_frag*/view_dir, direct_light_dir, k_d, k_s, alpha, voxel_norm, voxel_lighting);
float computed_shadow = ShadowCalculationPoint(i, -difference, wnorm, wpos/*, light_distance*/);
// directed_light += is_direct ? max(computed_shadow, /*LIGHT_AMBIANCE*/0.0) * direct_light * square_factor : vec3(0.0);
directed_light += is_direct ? mix(LIGHT_AMBIANCE, 1.0, computed_shadow) * direct_light * square_factor : vec3(0.0);
#ifdef FIGURE_SHADER
vec3 ambiance = color * 0.5 / distance_2; // Non-physical hack, but it's pretty subtle and *damn* does it make shadows on characters look better
#else
vec3 ambiance = vec3(0.0);
#endif
directed_light += (is_direct ? mix(LIGHT_AMBIANCE, 1.0, computed_shadow) * direct_light * square_factor : vec3(0.0)) + ambiance;
// directed_light += (is_direct ? 1.0 : LIGHT_AMBIANCE) * max(computed_shadow, /*LIGHT_AMBIANCE*/0.0) * direct_light * square_factor;// : vec3(0.0);
// directed_light += mix(LIGHT_AMBIANCE, 1.0, computed_shadow) * direct_light * square_factor;
// ambient_light += is_direct ? vec3(0.0) : vec3(0.0); // direct_light * square_factor * LIGHT_AMBIANCE;

View File

@ -26,6 +26,11 @@ float hash_fast(uvec3 q)
return float(n) * (1.0/float(0xffffffffU));
}
// 2D, but using shifted 2D textures
float noise_2d(vec2 pos) {
return texture(t_noise, pos).x;
}
// 3D, but using shifted 2D textures
float noise_3d(vec3 pos) {
pos.z *= 15.0;

View File

@ -44,8 +44,15 @@ const float UNDERWATER_MIST_DIST = 100.0;
const float PERSISTENT_AMBIANCE = 1.0 / 32.0;// 1.0 / 80; // 1.0 / 512; // 0.00125 // 0.1;// 0.025; // 0.1;
// Glow from static light sources
// Allowed to be > 1 due to HDR
const vec3 GLOW_COLOR = vec3(2, 1.30, 0.1);
const vec3 GLOW_COLOR = vec3(3.0, 0.9, 0.05);
// Calculate glow from static light sources, + some noise for flickering.
// TODO: Optionally disable the flickering for performance?
vec3 glow_light(vec3 pos) {
return GLOW_COLOR * (1.0 + (noise_3d(vec3(pos.xy * 0.005, tick.x * 0.5)) - 0.5) * 1.0);
}
//vec3 get_sun_dir(float time_of_day) {
// const float TIME_FACTOR = (PI * 2.0) / (3600.0 * 24.0);

View File

@ -1,6 +1,8 @@
#version 330 core
// #extension ARB_texture_storage : enable
#define FIGURE_SHADER
#include <constants.glsl>
#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION
@ -40,6 +42,7 @@ uniform u_locals {
mat4 model_mat;
vec4 highlight_col;
vec4 model_light;
vec4 model_glow;
ivec4 atlas_offs;
vec3 model_pos;
// bit 0 - is player

View File

@ -640,7 +640,7 @@ void main() {
// f_col = f_col + (hash(vec4(floor(vec3(focus_pos.xy + splay(v_pos_orig), f_pos.z)) * 3.0 - round(f_norm) * 0.5, 0)) - 0.5) * 0.05; // Small-scale noise
vec3 surf_color;
#if (FLUID_MODE == FLUID_MODE_SHINY)
if (f_col_raw.b > max(f_col_raw.r, f_col_raw.g) * 2.0 && dot(vec3(0, 0, 1), f_norm) > 0.9) {
if (length(f_col_raw - vec3(0.02, 0.06, 0.22)) < 0.025 && dot(vec3(0, 0, 1), f_norm) > 0.9) {
vec3 water_color = (1.0 - MU_WATER) * MU_SCATTER;
vec3 reflect_ray = cam_to_frag * vec3(1, 1, -1);

View File

@ -275,7 +275,7 @@ void main() {
vec3(0, 0, -2)
) + vec3(sin(lifetime), sin(lifetime + 0.7), sin(lifetime * 0.5)) * 2.0,
vec3(4),
vec4(vec3(0.2 + rand7 * 0.2, 0.2 + (0.5 + rand6 * 0.5) * 0.6, 0), 1),
vec4(vec3(0.2 + rand7 * 0.2, 0.2 + (0.25 + rand6 * 0.5) * 0.3, 0) * (0.75 + rand1 * 0.5), 1),
spin_in_axis(vec3(rand6, rand7, rand8), rand9 * 3 + lifetime * 5)
);
} else if (inst_mode == SNOW) {

View File

@ -1,5 +1,7 @@
#version 330 core
#define FIGURE_SHADER
#include <constants.glsl>
#define LIGHTING_TYPE LIGHTING_TYPE_REFLECTION
@ -26,6 +28,7 @@ uniform u_locals {
mat4 model_mat;
vec4 highlight_col;
vec4 model_light;
vec4 model_glow;
ivec4 atlas_offs;
vec3 model_pos;
int flags;

View File

@ -174,7 +174,7 @@ void main() {
reflected_light += point_light; */
// float ao = /*pow(f_ao, 0.5)*/f_ao * 0.85 + 0.15;
vec3 glow = pow(f_inst_light.y, 3) * 4 * GLOW_COLOR;
vec3 glow = pow(f_inst_light.y, 3) * 4 * glow_light(f_pos);
emitted_light += glow;
float ao = f_ao;

View File

@ -266,8 +266,7 @@ void main() {
max_light *= f_light;
// TODO: Apply AO after this
vec3 glow = GLOW_COLOR * (pow(f_glow, 6) * 8 + pow(f_glow, 2) * 0.5);
emitted_light += glow;
vec3 glow = glow_light(f_pos) * (pow(f_glow, 6) * 5 + pow(f_glow, 1.5) * 2);
reflected_light += glow;
max_light += lights_at(f_pos, f_norm, view_dir, mu, cam_attenuation, fluid_alt, k_a, k_d, k_s, alpha, f_norm, 1.0, emitted_light, reflected_light);

BIN
assets/voxygen/voxel/sprite/lantern/lantern-orange.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -2824,4 +2824,16 @@ SapphireSmall: Some((
],
wind_sway: 0.0,
)),
// Lantern
Lantern: Some((
variations: [
(
model: "voxygen.voxel.sprite.lantern.lantern-orange",
offset: (-2.5, -2.5, 0.0),
lod_axes: (0.0, 0.0, 0.0),
),
],
wind_sway: 0.0,
)),
)

View File

@ -138,4 +138,5 @@ pub enum SiteKind {
Dungeon { difficulty: u32 },
Castle,
Cave,
Tree,
}

View File

@ -32,6 +32,13 @@ impl<T> FromIterator<T> for Path<T> {
}
}
impl<T> IntoIterator for Path<T> {
type IntoIter = std::vec::IntoIter<T>;
type Item = T;
fn into_iter(self) -> Self::IntoIter { self.nodes.into_iter() }
}
impl<T> Path<T> {
pub fn is_empty(&self) -> bool { self.nodes.is_empty() }

View File

@ -1,73 +1,187 @@
use std::{
cmp::{Eq, PartialEq},
cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd},
fmt, hash,
marker::PhantomData,
ops::{Index, IndexMut},
};
// NOTE: We use u64 to make sure we are consistent across all machines. We
// assume that usize fits into 8 bytes.
pub struct Id<T>(u64, PhantomData<T>);
pub struct Id<T> {
idx: u32,
gen: u32,
phantom: PhantomData<T>,
}
impl<T> Id<T> {
pub fn id(&self) -> u64 { self.0 }
pub fn id(&self) -> u64 { self.idx as u64 | ((self.gen as u64) << 32) }
}
impl<T> Copy for Id<T> {}
impl<T> Clone for Id<T> {
fn clone(&self) -> Self { Self(self.0, PhantomData) }
fn clone(&self) -> Self {
Self {
idx: self.idx,
gen: self.gen,
phantom: PhantomData,
}
}
}
impl<T> Eq for Id<T> {}
impl<T> PartialEq for Id<T> {
fn eq(&self, other: &Self) -> bool { self.0 == other.0 }
fn eq(&self, other: &Self) -> bool { self.idx == other.idx && self.gen == other.gen }
}
impl<T> Ord for Id<T> {
fn cmp(&self, other: &Self) -> Ordering { (self.idx, self.gen).cmp(&(other.idx, other.gen)) }
}
impl<T> PartialOrd for Id<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
}
impl<T> fmt::Debug for Id<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Id<{}>({})", std::any::type_name::<T>(), self.0)
write!(
f,
"Id<{}>({}, {})",
std::any::type_name::<T>(),
self.idx,
self.gen
)
}
}
impl<T> hash::Hash for Id<T> {
fn hash<H: hash::Hasher>(&self, h: &mut H) { self.0.hash(h); }
fn hash<H: hash::Hasher>(&self, h: &mut H) {
self.idx.hash(h);
self.gen.hash(h);
}
}
struct Entry<T> {
gen: u32,
item: Option<T>,
}
pub struct Store<T> {
items: Vec<T>,
entries: Vec<Entry<T>>,
len: usize,
}
impl<T> Default for Store<T> {
fn default() -> Self { Self { items: Vec::new() } }
fn default() -> Self {
Self {
entries: Vec::new(),
len: 0,
}
}
}
impl<T> Store<T> {
pub fn is_empty(&self) -> bool { self.len == 0 }
pub fn len(&self) -> usize { self.len }
pub fn contains(&self, id: Id<T>) -> bool {
self.entries
.get(id.idx as usize)
.map(|e| e.gen == id.gen)
.unwrap_or(false)
}
pub fn get(&self, id: Id<T>) -> &T {
// NOTE: Safe conversion, because it came from usize.
self.items.get(id.0 as usize).unwrap()
let entry = self.entries.get(id.idx as usize).unwrap();
if entry.gen == id.gen {
entry.item.as_ref().unwrap()
} else {
panic!("Stale ID used to access store entry");
}
}
pub fn get_mut(&mut self, id: Id<T>) -> &mut T {
// NOTE: Safe conversion, because it came from usize.
self.items.get_mut(id.0 as usize).unwrap()
let entry = self.entries.get_mut(id.idx as usize).unwrap();
if entry.gen == id.gen {
entry.item.as_mut().unwrap()
} else {
panic!("Stale ID used to access store entry");
}
}
pub fn ids(&self) -> impl Iterator<Item = Id<T>> {
(0..self.items.len()).map(|i| Id(i as u64, PhantomData))
pub fn ids(&self) -> impl Iterator<Item = Id<T>> + '_ { self.iter().map(|(id, _)| id) }
pub fn values(&self) -> impl Iterator<Item = &T> + '_ { self.iter().map(|(_, item)| item) }
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> + '_ {
self.iter_mut().map(|(_, item)| item)
}
pub fn values(&self) -> impl Iterator<Item = &T> { self.items.iter() }
pub fn iter(&self) -> impl Iterator<Item = (Id<T>, &T)> + '_ {
self.entries
.iter()
.enumerate()
.filter_map(move |(idx, entry)| {
Some(Id {
idx: idx as u32,
gen: entry.gen,
phantom: PhantomData,
})
.zip(entry.item.as_ref())
})
}
pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> { self.items.iter_mut() }
pub fn iter(&self) -> impl Iterator<Item = (Id<T>, &T)> { self.ids().zip(self.values()) }
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Id<T>, &mut T)> {
self.ids().zip(self.values_mut())
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Id<T>, &mut T)> + '_ {
self.entries
.iter_mut()
.enumerate()
.filter_map(move |(idx, entry)| {
Some(Id {
idx: idx as u32,
gen: entry.gen,
phantom: PhantomData,
})
.zip(entry.item.as_mut())
})
}
pub fn insert(&mut self, item: T) -> Id<T> {
// NOTE: Assumes usize fits into 8 bytes.
let id = Id(self.items.len() as u64, PhantomData);
self.items.push(item);
id
if self.len < self.entries.len() {
// TODO: Make this more efficient with a lookahead system
let (idx, entry) = self
.entries
.iter_mut()
.enumerate()
.find(|(_, e)| e.item.is_none())
.unwrap();
entry.item = Some(item);
assert!(entry.gen < u32::MAX);
entry.gen += 1;
Id {
idx: idx as u32,
gen: entry.gen,
phantom: PhantomData,
}
} else {
assert!(self.entries.len() < (u32::MAX - 1) as usize);
let id = Id {
idx: self.entries.len() as u32,
gen: 0,
phantom: PhantomData,
};
self.entries.push(Entry {
gen: 0,
item: Some(item),
});
self.len += 1;
id
}
}
pub fn remove(&mut self, id: Id<T>) -> Option<T> {
if let Some(item) = self
.entries
.get_mut(id.idx as usize)
.and_then(|e| if e.gen == id.gen { e.item.take() } else { None })
{
self.len -= 1;
Some(item)
} else {
None
}
}
}

View File

@ -189,10 +189,24 @@ impl Block {
| SpriteKind::RubySmall
| SpriteKind::EmeraldSmall
| SpriteKind::SapphireSmall => Some(3),
SpriteKind::Lantern => Some(24),
_ => None,
}
}
// minimum block, attenuation
#[inline]
pub fn get_max_sunlight(&self) -> (u8, u8) {
match self.kind() {
BlockKind::Water => (1, 1),
BlockKind::Leaves => (9, 255),
BlockKind::Wood => (6, 2),
BlockKind::Snow => (6, 2),
_ if self.is_opaque() => (0, 255),
_ => (0, 0),
}
}
#[inline]
pub fn is_solid(&self) -> bool {
self.get_sprite()

View File

@ -142,6 +142,7 @@ make_case_elim!(
Seagrass = 0x73,
RedAlgae = 0x74,
UnderwaterVent = 0x75,
Lantern = 0x76,
}
);
@ -201,6 +202,7 @@ impl SpriteKind {
| SpriteKind::DropGate => 1.0,
// TODO: Figure out if this should be solid or not.
SpriteKind::Shelf => 1.0,
SpriteKind::Lantern => 0.9,
_ => return None,
})
}
@ -289,6 +291,7 @@ impl SpriteKind {
| SpriteKind::Bowl
| SpriteKind::VialEmpty
| SpriteKind::FireBowlGround
| SpriteKind::Lantern
)
}
}

View File

@ -30,11 +30,15 @@ make_case_elim!(
Hollow = 13,
Liana = 14,
Normal(color: Rgb<u8>) = 15,
Log = 16,
Block(kind: BlockKind, color: Rgb<u8>) = 17,
}
);
#[derive(Debug)]
pub enum StructureError {}
pub enum StructureError {
OutOfBounds,
}
#[derive(Clone)]
pub struct Structure {
@ -44,7 +48,6 @@ pub struct Structure {
struct BaseStructure {
vol: Dyna<StructureBlock, ()>,
empty: StructureBlock,
default_kind: BlockKind,
}
@ -109,7 +112,7 @@ impl ReadVol for Structure {
fn get(&self, pos: Vec3<i32>) -> Result<&Self::Vox, StructureError> {
match self.base.vol.get(pos + self.center) {
Ok(block) => Ok(block),
Err(DynaError::OutOfBounds) => Ok(&self.base.empty),
Err(DynaError::OutOfBounds) => Err(StructureError::OutOfBounds),
}
}
}
@ -165,13 +168,11 @@ impl assets::Compound for BaseStructure {
Ok(BaseStructure {
vol,
empty: StructureBlock::None,
default_kind: BlockKind::Misc,
})
} else {
Ok(BaseStructure {
vol: Dyna::filled(Vec3::zero(), StructureBlock::None, ()),
empty: StructureBlock::None,
default_kind: BlockKind::Misc,
})
}

View File

@ -26,7 +26,7 @@ impl<V: RectRasterableVol> VolGrid2d<V> {
#[inline(always)]
pub fn chunk_key<P: Into<Vec2<i32>>>(pos: P) -> Vec2<i32> {
pos.into()
.map2(V::RECT_SIZE, |e, sz: u32| e >> (sz - 1).count_ones())
.map2(V::RECT_SIZE, |e, sz: u32| e.div_euclid(sz as i32))
}
#[inline(always)]

View File

@ -2,7 +2,7 @@ pub use ::vek::{
bezier::repr_simd::*, geom::repr_simd::*, mat::repr_simd::column_major::Mat4, ops::*,
quaternion::repr_simd::*, transform::repr_simd::*, transition::*, vec::repr_simd::*,
};
/* pub use ::vek::{
/*pub use ::vek::{
bezier::repr_c::*, geom::repr_c::*, mat::repr_c::column_major::Mat4, ops::*,
quaternion::repr_c::*, transform::repr_c::*, transition::*, vec::repr_c::*,
}; */
};*/

View File

@ -364,6 +364,9 @@ image_ids! {
mmap_site_cave_bg: "voxygen.element.map.cave_bg",
mmap_site_cave_hover: "voxygen.element.map.cave_hover",
mmap_site_cave: "voxygen.element.map.cave",
mmap_site_excl: "voxygen.element.map.excl",
mmap_site_tree: "voxygen.element.map.tree",
mmap_site_tree_hover: "voxygen.element.map.tree_hover",
// Window Parts
window_3: "voxygen.element.frames.window_3",

View File

@ -49,6 +49,12 @@ widget_ids! {
show_dungeons_img,
show_dungeons_box,
show_dungeons_text,
show_caves_img,
show_caves_box,
show_caves_text,
show_trees_img,
show_trees_box,
show_trees_text,
show_difficulty_img,
show_difficulty_box,
show_difficulty_text,
@ -57,9 +63,6 @@ widget_ids! {
drag_ico,
zoom_txt,
zoom_ico,
show_caves_img,
show_caves_box,
show_caves_text,
}
}
@ -117,6 +120,7 @@ pub enum Event {
ShowCastles(bool),
ShowDungeons(bool),
ShowCaves(bool),
ShowTrees(bool),
Close,
}
@ -143,6 +147,7 @@ impl<'a> Widget for Map<'a> {
let show_dungeons = self.global_state.settings.gameplay.map_show_dungeons;
let show_castles = self.global_state.settings.gameplay.map_show_castles;
let show_caves = self.global_state.settings.gameplay.map_show_caves;
let show_trees = self.global_state.settings.gameplay.map_show_trees;
let mut events = Vec::new();
let i18n = &self.localized_strings;
// Tooltips
@ -471,6 +476,40 @@ impl<'a> Widget for Map<'a> {
.graphics_for(state.ids.show_caves_box)
.color(TEXT_COLOR)
.set(state.ids.show_caves_text, ui);
// Trees
Image::new(self.imgs.mmap_site_tree)
.down_from(state.ids.show_caves_img, 10.0)
.w_h(20.0, 20.0)
.set(state.ids.show_trees_img, ui);
if Button::image(if show_trees {
self.imgs.checkbox_checked
} else {
self.imgs.checkbox
})
.w_h(18.0, 18.0)
.hover_image(if show_trees {
self.imgs.checkbox_checked_mo
} else {
self.imgs.checkbox_mo
})
.press_image(if show_trees {
self.imgs.checkbox_checked
} else {
self.imgs.checkbox_press
})
.right_from(state.ids.show_trees_img, 10.0)
.set(state.ids.show_trees_box, ui)
.was_clicked()
{
events.push(Event::ShowTrees(!show_trees));
}
Text::new(i18n.get("hud.map.trees"))
.right_from(state.ids.show_trees_box, 10.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.graphics_for(state.ids.show_trees_box)
.color(TEXT_COLOR)
.set(state.ids.show_trees_text, ui);
// Map icons
if state.ids.mmap_site_icons.len() < self.client.sites().len() {
state.update(|state| {
@ -512,6 +551,7 @@ impl<'a> Widget for Map<'a> {
SiteKind::Dungeon { .. } => i18n.get("hud.map.dungeon"),
SiteKind::Castle => i18n.get("hud.map.castle"),
SiteKind::Cave => i18n.get("hud.map.cave"),
SiteKind::Tree => i18n.get("hud.map.tree"),
});
let (difficulty, desc) = match &site.kind {
SiteKind::Town => (0, i18n.get("hud.map.town").to_string()),
@ -522,12 +562,14 @@ impl<'a> Widget for Map<'a> {
),
SiteKind::Castle => (0, i18n.get("hud.map.castle").to_string()),
SiteKind::Cave => (0, i18n.get("hud.map.cave").to_string()),
SiteKind::Tree => (0, i18n.get("hud.map.tree").to_string()),
};
let site_btn = Button::image(match &site.kind {
SiteKind::Town => self.imgs.mmap_site_town,
SiteKind::Dungeon { .. } => self.imgs.mmap_site_dungeon,
SiteKind::Castle => self.imgs.mmap_site_castle,
SiteKind::Cave => self.imgs.mmap_site_cave,
SiteKind::Tree => self.imgs.mmap_site_tree,
})
.x_y_position_relative_to(
state.ids.grid,
@ -540,6 +582,7 @@ impl<'a> Widget for Map<'a> {
SiteKind::Dungeon { .. } => self.imgs.mmap_site_dungeon_hover,
SiteKind::Castle => self.imgs.mmap_site_castle_hover,
SiteKind::Cave => self.imgs.mmap_site_cave_hover,
SiteKind::Tree => self.imgs.mmap_site_tree_hover,
})
.image_color(UI_HIGHLIGHT_0)
.with_tooltip(
@ -560,6 +603,7 @@ impl<'a> Widget for Map<'a> {
_ => TEXT_COLOR,
},
SiteKind::Cave => TEXT_COLOR,
SiteKind::Tree => TEXT_COLOR,
},
);
// Only display sites that are toggled on
@ -584,6 +628,11 @@ impl<'a> Widget for Map<'a> {
site_btn.set(state.ids.mmap_site_icons[i], ui);
}
},
SiteKind::Tree => {
if show_trees {
site_btn.set(state.ids.mmap_site_icons[i], ui);
}
},
}
// Difficulty from 0-6
@ -640,6 +689,11 @@ impl<'a> Widget for Map<'a> {
dif_img.set(state.ids.site_difs[i], ui)
}
},
SiteKind::Tree => {
if show_trees {
dif_img.set(state.ids.site_difs[i], ui)
}
},
}
}
}

View File

@ -303,6 +303,7 @@ impl<'a> Widget for MiniMap<'a> {
SiteKind::Dungeon { .. } => self.imgs.mmap_site_dungeon_bg,
SiteKind::Castle => self.imgs.mmap_site_castle_bg,
SiteKind::Cave => self.imgs.mmap_site_cave_bg,
SiteKind::Tree => self.imgs.mmap_site_tree,
})
.x_y_position_relative_to(
state.ids.grid,
@ -323,6 +324,7 @@ impl<'a> Widget for MiniMap<'a> {
_ => Color::Rgba(1.0, 1.0, 1.0, 0.0),
},
SiteKind::Cave => Color::Rgba(1.0, 1.0, 1.0, 0.0),
SiteKind::Tree => Color::Rgba(1.0, 1.0, 1.0, 0.0),
}))
.parent(state.ids.grid)
.set(state.ids.mmap_site_icons_bgs[i], ui);
@ -331,6 +333,7 @@ impl<'a> Widget for MiniMap<'a> {
SiteKind::Dungeon { .. } => self.imgs.mmap_site_dungeon,
SiteKind::Castle => self.imgs.mmap_site_castle,
SiteKind::Cave => self.imgs.mmap_site_cave,
SiteKind::Tree => self.imgs.mmap_site_tree,
})
.middle_of(state.ids.mmap_site_icons_bgs[i])
.w_h(20.0, 20.0)

View File

@ -362,6 +362,7 @@ pub enum Event {
MapShowDungeons(bool),
MapShowCastles(bool),
MapShowCaves(bool),
MapShowTrees(bool),
AdjustWindowSize([u16; 2]),
ChangeFullscreenMode(FullScreenSettings),
ToggleParticlesEnabled(bool),
@ -2642,6 +2643,9 @@ impl Hud {
map::Event::ShowCaves(map_show_caves) => {
events.push(Event::MapShowCaves(map_show_caves));
},
map::Event::ShowTrees(map_show_trees) => {
events.push(Event::MapShowTrees(map_show_trees));
},
}
}
} else {

View File

@ -30,8 +30,8 @@ enum FaceKind {
Fluid,
}
const SUNLIGHT: u8 = 24;
const MAX_LIGHT_DIST: i32 = SUNLIGHT as i32;
pub const SUNLIGHT: u8 = 24;
pub const MAX_LIGHT_DIST: i32 = SUNLIGHT as i32;
fn calc_light<V: RectRasterableVol<Vox = Block> + ReadVol + Debug>(
is_sunlight: bool,
@ -69,25 +69,24 @@ fn calc_light<V: RectRasterableVol<Vox = Block> + ReadVol + Debug>(
if is_sunlight {
for x in 0..outer.size().w {
for y in 0..outer.size().h {
let z = outer.size().d - 1;
let is_air = vol_cached
.get(outer.min + Vec3::new(x, y, z))
.ok()
.map_or(false, |b| b.is_air());
let mut light = SUNLIGHT;
for z in (0..outer.size().d).rev() {
let (min_light, attenuation) = vol_cached
.get(outer.min + Vec3::new(x, y, z))
.map_or((0, 0), |b| b.get_max_sunlight());
light_map[lm_idx(x, y, z)] = if is_air {
if vol_cached
.get(outer.min + Vec3::new(x, y, z - 1))
.ok()
.map_or(false, |b| b.is_air())
{
light_map[lm_idx(x, y, z - 1)] = SUNLIGHT;
if light > min_light {
light = light.saturating_sub(attenuation).max(min_light);
}
light_map[lm_idx(x, y, z)] = light;
if light == 0 {
break;
} else {
prop_que.push_back((x as u8, y as u8, z as u16));
}
SUNLIGHT
} else {
OPAQUE
};
}
}
}
}
@ -128,46 +127,26 @@ fn calc_light<V: RectRasterableVol<Vox = Block> + ReadVol + Debug>(
let pos = Vec3::new(pos.0 as i32, pos.1 as i32, pos.2 as i32);
let light = light_map[lm_idx(pos.x, pos.y, pos.z)];
// If ray propagate downwards at full strength
if is_sunlight && light == SUNLIGHT {
// Down is special cased and we know up is a ray
// Special cased ray propagation
let pos = Vec3::new(pos.x, pos.y, pos.z - 1);
let (is_air, is_liquid) = vol_cached
.get(outer.min + pos)
.ok()
.map_or((false, false), |b| (b.is_air(), b.is_liquid()));
light_map[lm_idx(pos.x, pos.y, pos.z)] = if is_air {
prop_que.push_back((pos.x as u8, pos.y as u8, pos.z as u16));
SUNLIGHT
} else if is_liquid {
prop_que.push_back((pos.x as u8, pos.y as u8, pos.z as u16));
SUNLIGHT - 1
} else {
OPAQUE
}
} else {
// Up
// Bounds checking
if pos.z + 1 < outer.size().d {
propagate(
light,
light_map.get_mut(lm_idx(pos.x, pos.y, pos.z + 1)).unwrap(),
Vec3::new(pos.x, pos.y, pos.z + 1),
&mut prop_que,
&mut vol_cached,
)
}
// Down
if pos.z > 0 {
propagate(
light,
light_map.get_mut(lm_idx(pos.x, pos.y, pos.z - 1)).unwrap(),
Vec3::new(pos.x, pos.y, pos.z - 1),
&mut prop_que,
&mut vol_cached,
)
}
// Up
// Bounds checking
if pos.z + 1 < outer.size().d {
propagate(
light,
light_map.get_mut(lm_idx(pos.x, pos.y, pos.z + 1)).unwrap(),
Vec3::new(pos.x, pos.y, pos.z + 1),
&mut prop_que,
&mut vol_cached,
)
}
// Down
if pos.z > 0 {
propagate(
light,
light_map.get_mut(lm_idx(pos.x, pos.y, pos.z - 1)).unwrap(),
Vec3::new(pos.x, pos.y, pos.z - 1),
&mut prop_que,
&mut vol_cached,
)
}
// The XY directions
if pos.y + 1 < outer.size().h {
@ -401,7 +380,13 @@ impl<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + 'static>
let greedy_size_cross = Vec3::new(greedy_size.x - 1, greedy_size.y - 1, greedy_size.z);
let draw_delta = Vec3::new(1, 1, z_start);
let get_light = |_: &mut (), pos: Vec3<i32>| light(pos + range.min);
let get_light = |_: &mut (), pos: Vec3<i32>| {
if flat_get(pos).is_opaque() {
0.0
} else {
light(pos + range.min)
}
};
let get_glow = |_: &mut (), pos: Vec3<i32>| glow(pos + range.min);
let get_color =
|_: &mut (), pos: Vec3<i32>| flat_get(pos).get_color().unwrap_or(Rgb::zero());

View File

@ -14,6 +14,7 @@ gfx_defines! {
model_mat: [[f32; 4]; 4] = "model_mat",
highlight_col: [f32; 4] = "highlight_col",
model_light: [f32; 4] = "model_light",
model_glow: [f32; 4] = "model_glow",
atlas_offs: [i32; 4] = "atlas_offs",
model_pos: [f32; 3] = "model_pos",
flags: u32 = "flags",
@ -60,7 +61,7 @@ impl Locals {
atlas_offs: Vec2<i32>,
is_player: bool,
light: f32,
glow: f32,
glow: (Vec3<f32>, f32),
) -> Self {
let mut flags = 0;
flags |= is_player as u32;
@ -70,7 +71,8 @@ impl Locals {
highlight_col: [col.r, col.g, col.b, 1.0],
model_pos: pos.into_array(),
atlas_offs: Vec4::from(atlas_offs).into_array(),
model_light: [light, glow, 1.0, 1.0],
model_light: [light, 1.0, 1.0, 1.0],
model_glow: [glow.0.x, glow.0.y, glow.0.z, glow.1],
flags,
}
}
@ -85,7 +87,7 @@ impl Default for Locals {
Vec2::default(),
false,
1.0,
0.0,
(Vec3::zero(), 0.0),
)
}
}

View File

@ -4602,7 +4602,7 @@ pub struct FigureStateMeta {
last_pos: Option<anim::vek::Vec3<f32>>,
avg_vel: anim::vek::Vec3<f32>,
last_light: f32,
last_glow: f32,
last_glow: (Vec3<f32>, f32),
acc_vel: f32,
}
@ -4649,7 +4649,7 @@ impl<S: Skeleton> FigureState<S> {
last_pos: None,
avg_vel: anim::vek::Vec3::zero(),
last_light: 1.0,
last_glow: 0.0,
last_glow: (Vec3::zero(), 0.0),
acc_vel: 0.0,
},
skeleton,
@ -4743,14 +4743,15 @@ impl<S: Skeleton> FigureState<S> {
let s = Lerp::lerp(s_10, s_11, (wpos.x.fract() - 0.5).abs() * 2.0);
*/
Vec2::new(t.light_at_wpos(wposi), t.glow_at_wpos(wposi)).into_tuple()
(t.light_at_wpos(wposi), t.glow_normal_at_wpos(wpos))
})
.unwrap_or((1.0, 0.0));
.unwrap_or((1.0, (Vec3::zero(), 0.0)));
// Fade between light and glow levels
// TODO: Making this temporal rather than spatial is a bit dumb but it's a very
// subtle difference
self.last_light = vek::Lerp::lerp(self.last_light, light, 16.0 * dt);
self.last_glow = vek::Lerp::lerp(self.last_glow, glow, 16.0 * dt);
self.last_glow.0 = vek::Lerp::lerp(self.last_glow.0, glow.0, 16.0 * dt);
self.last_glow.1 = vek::Lerp::lerp(self.last_glow.1, glow.1, 16.0 * dt);
let locals = FigureLocals::new(
mat,

View File

@ -2,7 +2,7 @@ use core::{iter, mem};
use hashbrown::HashMap;
use num::traits::Float;
pub use vek::{geom::repr_simd::*, mat::repr_simd::column_major::Mat4, ops::*, vec::repr_simd::*};
// pub use vek::{geom::repr_c::*, mat::repr_c::column_major::Mat4, ops::*,
//pub use vek::{geom::repr_c::*, mat::repr_c::column_major::Mat4, ops::*,
// vec::repr_c::*};
pub fn aabb_to_points<T: Float>(bounds: Aabb<T>) -> [Vec3<T>; 8] {

View File

@ -571,7 +571,7 @@ impl ParticleMgr {
cond: |_| true,
},
BlockParticles {
blocks: |boi| &boi.reeds,
blocks: |boi| &boi.fireflies,
range: 6,
rate: 0.004,
lifetime: 40.0,

View File

@ -3,7 +3,7 @@ mod watcher;
pub use self::watcher::BlocksOfInterest;
use crate::{
mesh::{greedy::GreedyMesh, Meshable},
mesh::{greedy::GreedyMesh, terrain::SUNLIGHT, Meshable},
render::{
ColLightFmt, ColLightInfo, Consts, FluidPipeline, GlobalModel, Instances, Mesh, Model,
RenderError, Renderer, ShadowPipeline, SpriteInstance, SpriteLocals, SpritePipeline,
@ -508,6 +508,52 @@ impl<V: RectRasterableVol> Terrain<V> {
.unwrap_or(0.0)
}
pub fn glow_normal_at_wpos(&self, wpos: Vec3<f32>) -> (Vec3<f32>, f32) {
let wpos_chunk = wpos.xy().map2(TerrainChunk::RECT_SIZE, |e: f32, sz| {
(e as i32).div_euclid(sz as i32)
});
const AMBIANCE: f32 = 0.15; // 0-1, the proportion of light that should illuminate the rear of an object
let (bias, total) = Spiral2d::new()
.take(9)
.map(|rpos| {
let chunk_pos = wpos_chunk + rpos;
self.chunks
.get(&chunk_pos)
.map(|c| c.blocks_of_interest.lights.iter())
.into_iter()
.flatten()
.map(move |(lpos, level)| {
(
Vec3::<i32>::from(
chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
) + *lpos,
level,
)
})
})
.flatten()
.fold(
(Vec3::broadcast(0.001), 0.0),
|(bias, total), (lpos, level)| {
let rpos = lpos.map(|e| e as f32 + 0.5) - wpos;
let level = (*level as f32 - rpos.magnitude()).max(0.0) / SUNLIGHT as f32;
(
bias + rpos.try_normalized().unwrap_or_else(Vec3::zero) * level,
total + level,
)
},
);
let bias_factor = bias.magnitude() * (1.0 - AMBIANCE) / total.max(0.001);
(
bias.try_normalized().unwrap_or_else(Vec3::zero) * bias_factor.powf(0.5),
self.glow_at_wpos(wpos.map(|e| e.floor() as i32)),
)
}
/// Maintain terrain data. To be called once per tick.
#[allow(clippy::for_loops_over_fallibles)] // TODO: Pending review in #587
#[allow(clippy::len_zero)] // TODO: Pending review in #587
@ -596,13 +642,23 @@ impl<V: RectRasterableVol> Terrain<V> {
// be meshed
span!(guard, "Add chunks with modified blocks to mesh todo list");
// TODO: would be useful if modified blocks were grouped by chunk
for pos in scene_data
.state
.terrain_changes()
.modified_blocks
.iter()
.map(|(p, _)| *p)
{
for (&pos, &_block) in scene_data.state.terrain_changes().modified_blocks.iter() {
// TODO: Be cleverer about this to avoid remeshing all neighbours. There are a
// few things that can create an 'effect at a distance'. These are
// as follows:
// - A glowing block is added or removed, thereby causing a lighting
// recalculation proportional to its glow radius.
// - An opaque block that was blocking sunlight from entering a cavity is
// removed (or added) thereby
// changing the way that sunlight propagates into the cavity.
//
// We can and should be cleverer about this, but it's non-trivial. For now, just
// conservatively assume that the lighting in all neighbouring
// chunks is invalidated. Thankfully, this doesn't need to happen often
// because block modification is unusual in Veloren.
// let block_effect_radius = block.get_glow().unwrap_or(0).max(1);
let block_effect_radius = crate::mesh::terrain::MAX_LIGHT_DIST;
// Handle block changes on chunk borders
// Remesh all neighbours because we have complex lighting now
// TODO: if lighting is on the server this can be updated to only remesh when
@ -610,7 +666,7 @@ impl<V: RectRasterableVol> Terrain<V> {
// change was on the border
for x in -1..2 {
for y in -1..2 {
let neighbour_pos = pos + Vec3::new(x, y, 0);
let neighbour_pos = pos + Vec3::new(x, y, 0) * block_effect_radius;
let neighbour_chunk_pos = scene_data.state.terrain().pos_key(neighbour_pos);
// Only remesh if this chunk has all its neighbors

View File

@ -15,6 +15,7 @@ pub struct BlocksOfInterest {
pub smokers: Vec<Vec3<i32>>,
pub beehives: Vec<Vec3<i32>>,
pub reeds: Vec<Vec3<i32>>,
pub fireflies: Vec<Vec3<i32>>,
pub flowers: Vec<Vec3<i32>>,
pub fire_bowls: Vec<Vec3<i32>>,
pub snow: Vec<Vec3<i32>>,
@ -39,6 +40,7 @@ impl BlocksOfInterest {
let mut smokers = Vec::new();
let mut beehives = Vec::new();
let mut reeds = Vec::new();
let mut fireflies = Vec::new();
let mut flowers = Vec::new();
let mut interactables = Vec::new();
let mut lights = Vec::new();
@ -94,10 +96,12 @@ impl BlocksOfInterest {
Some(SpriteKind::Beehive) => beehives.push(pos),
Some(SpriteKind::Reed) => {
reeds.push(pos);
fireflies.push(pos);
if thread_rng().gen_range(0..12) == 0 {
frogs.push(pos)
frogs.push(pos);
}
},
Some(SpriteKind::CaveMushroom) => fireflies.push(pos),
Some(SpriteKind::PinkFlower) => flowers.push(pos),
Some(SpriteKind::PurpleFlower) => flowers.push(pos),
Some(SpriteKind::RedFlower) => flowers.push(pos),
@ -123,6 +127,7 @@ impl BlocksOfInterest {
smokers,
beehives,
reeds,
fireflies,
flowers,
interactables,
lights,

View File

@ -1266,6 +1266,10 @@ impl PlayState for SessionState {
global_state.settings.gameplay.map_show_caves = map_show_caves;
global_state.settings.save_to_file_warn();
},
HudEvent::MapShowTrees(map_show_trees) => {
global_state.settings.gameplay.map_show_trees = map_show_trees;
global_state.settings.save_to_file_warn();
},
HudEvent::ChangeGamma(new_gamma) => {
global_state.settings.graphics.gamma = new_gamma;
global_state.settings.save_to_file_warn();

View File

@ -463,6 +463,7 @@ pub struct GameplaySettings {
pub map_show_castles: bool,
pub loading_tips: bool,
pub map_show_caves: bool,
pub map_show_trees: bool,
pub minimap_show: bool,
pub minimap_face_north: bool,
}
@ -502,6 +503,7 @@ impl Default for GameplaySettings {
map_show_castles: true,
loading_tips: true,
map_show_caves: true,
map_show_trees: true,
minimap_show: true,
minimap_face_north: false,
}

View File

@ -14,6 +14,7 @@ common = { package = "veloren-common", path = "../common" }
common-net = { package = "veloren-common-net", path = "../common/net" }
bincode = "1.3.1"
bitvec = "0.21.0"
enum-iterator = "0.6"
fxhash = "0.2.1"
image = { version = "0.23.12", default-features = false, features = ["png"] }
itertools = "0.10"
@ -36,3 +37,9 @@ ron = { version = "0.6", default-features = false }
criterion = "0.3"
tracing-subscriber = { version = "0.2.3", default-features = false, features = ["fmt", "chrono", "ansi", "smallvec"] }
minifb = "0.19.1"
svg_fmt = "0.4"
structopt = "0.3"
[[bench]]
harness = false
name = "tree"

41
world/benches/tree.rs Normal file
View File

@ -0,0 +1,41 @@
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
use rand::prelude::*;
use veloren_world::layer::tree::{ProceduralTree, TreeConfig};
fn tree(c: &mut Criterion) {
c.bench_function("generate", |b| {
let mut i = 0;
b.iter(|| {
i += 1;
black_box(ProceduralTree::generate(
TreeConfig::oak(&mut thread_rng(), 1.0),
&mut thread_rng(),
));
});
});
c.bench_function("sample", |b| {
let mut i = 0;
b.iter_batched(
|| {
i += 1;
ProceduralTree::generate(TreeConfig::oak(&mut thread_rng(), 1.0), &mut thread_rng())
},
|tree| {
let bounds = tree.get_bounds();
for x in (bounds.min.x as i32..bounds.max.x as i32).step_by(3) {
for y in (bounds.min.y as i32..bounds.max.y as i32).step_by(3) {
for z in (bounds.min.z as i32..bounds.max.z as i32).step_by(3) {
let pos = (x as f32, y as f32, z as f32).into();
black_box(tree.is_branch_or_leaves_at(pos));
}
}
}
},
BatchSize::SmallInput,
);
});
}
criterion_group!(benches, tree);
criterion_main!(benches);

34
world/examples/site.rs Normal file
View File

@ -0,0 +1,34 @@
use svg_fmt::*;
use veloren_world::site2::test_site;
fn main() {
let site = test_site();
let size = site.bounds().size();
println!("{}", BeginSvg {
w: size.w as f32,
h: size.h as f32
});
for plot in site.plots() {
let bounds = plot.find_bounds();
println!("{}", Rectangle {
x: bounds.min.x as f32,
y: bounds.min.y as f32,
w: bounds.size().w as f32,
h: bounds.size().h as f32,
style: Style {
fill: Fill::Color(Color {
r: 50,
g: 50,
b: 50
}),
stroke: Stroke::Color(Color { r: 0, g: 0, b: 0 }, 1.0),
opacity: 1.0,
stroke_opacity: 1.0,
},
border_radius: 0.0,
});
}
println!("{}", EndSvg);
}

View File

@ -1,4 +1,9 @@
#[derive(Copy, Clone, Debug)]
use crate::util::math::close;
use enum_iterator::IntoEnumIterator;
use std::ops::Range;
use vek::*;
#[derive(Copy, Clone, Debug, IntoEnumIterator)]
pub enum ForestKind {
Palm,
Acacia,
@ -7,5 +12,82 @@ pub enum ForestKind {
Pine,
Birch,
Mangrove,
Giant,
Swamp,
}
pub struct Environment {
pub humid: f32,
pub temp: f32,
pub near_water: f32,
}
impl ForestKind {
pub fn humid_range(&self) -> Range<f32> {
match self {
ForestKind::Palm => 0.25..1.4,
ForestKind::Acacia => 0.05..0.55,
ForestKind::Baobab => 0.2..0.6,
ForestKind::Oak => 0.35..1.5,
ForestKind::Pine => 0.2..1.4,
ForestKind::Birch => 0.0..0.6,
ForestKind::Mangrove => 0.65..1.3,
ForestKind::Swamp => 0.5..1.1,
_ => 0.0..0.0,
}
}
pub fn temp_range(&self) -> Range<f32> {
match self {
ForestKind::Palm => 0.4..1.6,
ForestKind::Acacia => 0.3..1.6,
ForestKind::Baobab => 0.4..0.9,
ForestKind::Oak => -0.35..0.6,
ForestKind::Pine => -1.8..-0.2,
ForestKind::Birch => -0.7..0.25,
ForestKind::Mangrove => 0.4..1.6,
ForestKind::Swamp => -0.6..0.8,
_ => 0.0..0.0,
}
}
pub fn near_water_range(&self) -> Option<Range<f32>> {
match self {
ForestKind::Palm => Some(0.35..1.8),
ForestKind::Swamp => Some(0.5..1.8),
_ => None,
}
}
/// The relative rate at which this tree appears under ideal conditions
pub fn ideal_proclivity(&self) -> f32 {
match self {
ForestKind::Palm => 0.4,
ForestKind::Acacia => 0.6,
ForestKind::Baobab => 0.2,
ForestKind::Oak => 1.0,
ForestKind::Pine => 1.0,
ForestKind::Birch => 0.65,
ForestKind::Mangrove => 1.0,
ForestKind::Swamp => 1.0,
_ => 0.0,
}
}
pub fn proclivity(&self, env: &Environment) -> f32 {
self.ideal_proclivity()
* close(env.humid, self.humid_range())
* close(env.temp, self.temp_range())
* self.near_water_range().map_or(1.0, |near_water_range| {
close(env.near_water, near_water_range)
})
}
}
pub struct TreeAttr {
pub pos: Vec2<i32>,
pub seed: u32,
pub scale: f32,
pub forest_kind: ForestKind,
pub inhabited: bool,
}

View File

@ -1,6 +1,6 @@
use crate::{
column::{ColumnGen, ColumnSample},
util::{RandomField, Sampler, SmallCache},
util::{FastNoise, RandomField, Sampler, SmallCache},
IndexRef,
};
use common::terrain::{
@ -70,6 +70,8 @@ impl<'a> BlockGen<'a> {
// humidity,
stone_col,
snow_cover,
cliff_offset,
cliff_height,
..
} = sample;
@ -118,7 +120,28 @@ impl<'a> BlockGen<'a> {
.map(|e| (e * 255.0) as u8);
if stone_factor >= 0.5 {
Some(Block::new(BlockKind::Rock, col))
if wposf.z as f32 > height - cliff_offset.max(0.0) {
if cliff_offset.max(0.0)
> cliff_height
- (FastNoise::new(37).get(wposf / Vec3::new(6.0, 6.0, 10.0)) * 0.5
+ 0.5)
* (height - grass_depth - wposf.z as f32)
.mul(0.25)
.clamped(0.0, 8.0)
{
Some(Block::empty())
} else {
let col = Lerp::lerp(
col.map(|e| e as f32),
col.map(|e| e as f32) * 0.7,
(wposf.z as f32 - basement * 0.3).div(2.0).sin() * 0.5 + 0.5,
)
.map(|e| e as u8);
Some(Block::new(BlockKind::Rock, col))
}
} else {
Some(Block::new(BlockKind::Rock, col))
}
} else {
Some(Block::new(BlockKind::Earth, col))
}
@ -183,7 +206,9 @@ pub struct ZCache<'a> {
impl<'a> ZCache<'a> {
pub fn get_z_limits(&self) -> (f32, f32) {
let min = self.sample.alt - (self.sample.chaos.min(1.0) * 16.0);
let min = self.sample.alt
- (self.sample.chaos.min(1.0) * 16.0)
- self.sample.cliff_offset.max(0.0);
let min = min - 4.0;
let rocks = if self.sample.rock > 0.0 { 12.0 } else { 0.0 };
@ -220,6 +245,7 @@ pub fn block_from_structure(
sample.surface_color.map(|e| (e * 255.0) as u8),
)),
StructureBlock::Normal(color) => Some(Block::new(BlockKind::Misc, color)),
StructureBlock::Block(kind, color) => Some(Block::new(kind, color)),
StructureBlock::Water => Some(Block::water(SpriteKind::Empty)),
// TODO: If/when liquid supports other colors again, revisit this.
StructureBlock::GreenSludge => Some(Block::water(SpriteKind::Empty)),
@ -249,6 +275,7 @@ pub fn block_from_structure(
Some(with_sprite(SpriteKind::Chest))
}
},
StructureBlock::Log => Some(Block::new(BlockKind::Wood, Rgb::new(60, 30, 0))),
// We interpolate all these BlockKinds as needed.
StructureBlock::TemperateLeaves
| StructureBlock::PineLeaves

View File

@ -2,7 +2,8 @@ use crate::{
block::ZCache,
column::ColumnSample,
index::IndexRef,
sim::{SimChunk, WorldSim as Land},
land::Land,
sim::{SimChunk, WorldSim},
util::Grid,
};
use common::{
@ -17,7 +18,7 @@ pub struct CanvasInfo<'a> {
pub(crate) wpos: Vec2<i32>,
pub(crate) column_grid: &'a Grid<Option<ZCache<'a>>>,
pub(crate) column_grid_border: i32,
pub(crate) land: &'a Land,
pub(crate) chunks: &'a WorldSim,
pub(crate) index: IndexRef<'a>,
pub(crate) chunk: &'a SimChunk,
}
@ -45,7 +46,9 @@ impl<'a> CanvasInfo<'a> {
pub fn chunk(&self) -> &'a SimChunk { self.chunk }
pub fn land(&self) -> &'a Land { self.land }
pub fn chunks(&self) -> &'a WorldSim { self.chunks }
pub fn land(&self) -> Land<'_> { Land::from_sim(self.chunks) }
}
pub struct Canvas<'a> {
@ -60,8 +63,12 @@ impl<'a> Canvas<'a> {
/// inner `CanvasInfo` such that it may be used independently.
pub fn info(&mut self) -> CanvasInfo<'a> { self.info }
pub fn get(&mut self, pos: Vec3<i32>) -> Option<Block> {
self.chunk.get(pos - self.wpos()).ok().copied()
pub fn get(&mut self, pos: Vec3<i32>) -> Block {
self.chunk
.get(pos - self.wpos())
.ok()
.copied()
.unwrap_or_else(Block::empty)
}
pub fn set(&mut self, pos: Vec3<i32>, block: Block) {

View File

@ -6,9 +6,10 @@ use self::{Occupation::*, Stock::*};
use crate::{
config::CONFIG,
sim::WorldSim,
site::{namegen::NameGen, Castle, Dungeon, Settlement, Site as WorldSite},
site::{namegen::NameGen, Castle, Dungeon, Settlement, Site as WorldSite, Tree},
site2,
util::{attempt, seed_expan, MapVec, CARDINALS, NEIGHBORS},
Index,
Index, Land,
};
use common::{
astar::Astar,
@ -105,8 +106,10 @@ impl Civs {
for _ in 0..initial_civ_count * 3 {
attempt(5, || {
let (kind, size) = match ctx.rng.gen_range(0..8) {
0 => (SiteKind::Castle, 3),
let (kind, size) = match ctx.rng.gen_range(0..64) {
0..=4 => (SiteKind::Castle, 3),
// 5..=28 => (SiteKind::Refactor, 6),
29..=31 => (SiteKind::Tree, 4),
_ => (SiteKind::Dungeon, 0),
};
let loc = find_site_loc(&mut ctx, None, size)?;
@ -142,14 +145,14 @@ impl Civs {
// Flatten ground around sites
for site in this.sites.values() {
let radius = 48i32;
let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32);
let flatten_radius = match &site.kind {
SiteKind::Settlement => 10.0,
SiteKind::Dungeon => 2.0,
SiteKind::Castle => 5.0,
let (radius, flatten_radius) = match &site.kind {
SiteKind::Settlement => (32i32, 10.0),
SiteKind::Dungeon => (8i32, 2.0),
SiteKind::Castle => (16i32, 5.0),
SiteKind::Refactor => (0i32, 0.0),
SiteKind::Tree => (12i32, 8.0),
};
let (raise, raise_dist): (f32, i32) = match &site.kind {
@ -187,7 +190,6 @@ impl Civs {
chunk.alt += diff;
chunk.basement += diff;
chunk.rockiness = 0.0;
chunk.warp_factor = 0.0;
chunk.surface_veg *= 1.0 - factor * rng.gen_range(0.25..0.9);
});
}
@ -215,6 +217,14 @@ impl Civs {
SiteKind::Castle => {
WorldSite::castle(Castle::generate(wpos, Some(ctx.sim), &mut rng))
},
SiteKind::Refactor => WorldSite::refactor(site2::Site::generate(
&Land::from_sim(&ctx.sim),
&mut rng,
wpos,
)),
SiteKind::Tree => {
WorldSite::tree(Tree::generate(wpos, &Land::from_sim(&ctx.sim), &mut rng))
},
});
sim_site.site_tmp = Some(site);
let site_ref = &index.sites[site];
@ -694,7 +704,7 @@ fn walk_in_dir(sim: &WorldSim, a: Vec2<i32>, dir: Vec2<i32>) -> Option<f32> {
/// Return true if a position is suitable for walking on
fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2<i32>) -> bool {
if let Some(chunk) = sim.get(loc) {
!chunk.river.is_ocean() && !chunk.river.is_lake()
!chunk.river.is_ocean() && !chunk.river.is_lake() && !chunk.near_cliffs()
} else {
false
}
@ -893,6 +903,8 @@ pub enum SiteKind {
Settlement,
Dungeon,
Castle,
Refactor,
Tree,
}
impl Site {

View File

@ -69,10 +69,10 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let sim = &self.sim;
let _turb = Vec2::new(
sim.gen_ctx.turb_x_nz.get((wposf.div(48.0)).into_array()) as f32,
sim.gen_ctx.turb_y_nz.get((wposf.div(48.0)).into_array()) as f32,
) * 12.0;
// let turb = Vec2::new(
// sim.gen_ctx.turb_x_nz.get((wposf.div(48.0)).into_array()) as f32,
// sim.gen_ctx.turb_y_nz.get((wposf.div(48.0)).into_array()) as f32,
// ) * 12.0;
let wposf_turb = wposf; // + turb.map(|e| e as f64);
let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?;
@ -81,9 +81,13 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
let rockiness = sim.get_interpolated(wpos, |chunk| chunk.rockiness)?;
let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?;
let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?;
let near_water =
sim.get_interpolated(
wpos,
|chunk| if chunk.river.near_water() { 1.0 } else { 0.0 },
)?;
let alt = sim.get_interpolated_monotone(wpos, |chunk| chunk.alt)?;
let surface_veg = sim.get_interpolated_monotone(wpos, |chunk| chunk.surface_veg)?;
let chunk_warp_factor = sim.get_interpolated_monotone(wpos, |chunk| chunk.warp_factor)?;
let sim_chunk = sim.get(chunk_pos)?;
let neighbor_coef = TerrainChunkSize::RECT_SIZE.map(|e| e as f64);
let my_chunk_idx = vec2_as_uniform_idx(self.sim.map_size_lg(), chunk_pos);
@ -261,6 +265,27 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
)
});
// Cliffs
let cliff_factor = (alt
+ self.sim.gen_ctx.hill_nz.get(wposf.div(64.0).into_array()) as f32 * 8.0
+ self.sim.gen_ctx.hill_nz.get(wposf.div(350.0).into_array()) as f32 * 128.0)
.rem_euclid(200.0)
/ 64.0
- 1.0;
let cliff_scale =
((self.sim.gen_ctx.hill_nz.get(wposf.div(128.0).into_array()) as f32 * 1.5 + 0.75)
+ self.sim.gen_ctx.hill_nz.get(wposf.div(48.0).into_array()) as f32 * 0.1)
.clamped(0.0, 1.0)
.powf(2.0);
let cliff_height = sim.get_interpolated(wpos, |chunk| chunk.cliff_height)? * cliff_scale;
let cliff = if cliff_factor < 0.0 {
cliff_factor.abs().powf(1.5)
} else {
0.0
} * (1.0 - near_water * 3.0).max(0.0).powi(2);
let cliff_offset = cliff * cliff_height;
let alt = alt + (cliff - 0.5) * cliff_height;
// Find the average distance to each neighboring body of water.
let mut river_count = 0.0f64;
let mut overlap_count = 0.0f64;
@ -436,7 +461,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
.unwrap_or(CONFIG.sea_level);
let river_gouge = 0.5;
let (_in_water, water_dist, alt_, water_level, riverless_alt, warp_factor) = if let Some(
let (_in_water, water_dist, alt_, water_level, _riverless_alt, warp_factor) = if let Some(
(max_border_river_pos, river_chunk, max_border_river, max_border_river_dist),
) =
max_river
@ -701,13 +726,12 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
1.0,
)
};
let warp_factor = warp_factor * chunk_warp_factor;
// NOTE: To disable warp, uncomment this line.
// let warp_factor = 0.0;
let riverless_alt_delta = Lerp::lerp(0.0, riverless_alt_delta, warp_factor);
let riverless_alt = alt + riverless_alt_delta; //riverless_alt + riverless_alt_delta;
let alt = alt_ + riverless_alt_delta;
let riverless_alt = riverless_alt + riverless_alt_delta;
let basement =
alt + sim.get_interpolated_monotone(wpos, |chunk| chunk.basement.sub(chunk.alt))?;
@ -982,6 +1006,10 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
.map(|wd| Lerp::lerp(sub_surface_color, ground, (wd / 3.0).clamped(0.0, 1.0)))
.unwrap_or(ground);
// Ground under thick trees should be receive less sunlight and so often become
// dirt
let ground = Lerp::lerp(ground, sub_surface_color, marble_mid * tree_density);
let near_ocean = max_river.and_then(|(_, _, river_data, _)| {
if (river_data.is_lake() || river_data.river_kind == Some(RiverKind::Ocean))
&& alt <= water_level.max(CONFIG.sea_level + 5.0)
@ -1047,6 +1075,8 @@ impl<'a> Sampler<'a> for ColumnGen<'a> {
path,
cave,
snow_cover,
cliff_offset,
cliff_height,
chunk: sim_chunk,
})
@ -1077,6 +1107,8 @@ pub struct ColumnSample<'a> {
pub path: Option<(f32, Vec2<f32>, Path, Vec2<f32>)>,
pub cave: Option<(f32, Vec2<f32>, Cave, Vec2<f32>)>,
pub snow_cover: bool,
pub cliff_offset: f32,
pub cliff_height: f32,
pub chunk: &'a SimChunk,
}

35
world/src/land.rs Normal file
View File

@ -0,0 +1,35 @@
use crate::sim;
use common::{terrain::TerrainChunkSize, vol::RectVolSize};
use vek::*;
/// A wrapper type that may contain a reference to a generated world. If not,
/// default values will be provided.
pub struct Land<'a> {
sim: Option<&'a sim::WorldSim>,
}
impl<'a> Land<'a> {
pub fn empty() -> Self { Self { sim: None } }
pub fn from_sim(sim: &'a sim::WorldSim) -> Self { Self { sim: Some(sim) } }
pub fn get_alt_approx(&self, wpos: Vec2<i32>) -> f32 {
self.sim
.and_then(|sim| sim.get_alt_approx(wpos))
.unwrap_or(0.0)
}
pub fn get_gradient_approx(&self, wpos: Vec2<i32>) -> f32 {
self.sim
.and_then(|sim| {
sim.get_gradient_approx(
wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)),
)
})
.unwrap_or(0.0)
}
pub fn get_chunk_at(&self, wpos: Vec2<i32>) -> Option<&sim::SimChunk> {
self.sim.and_then(|sim| sim.get_wpos(wpos))
}
}

View File

@ -6,7 +6,7 @@ pub use self::{scatter::apply_scatter_to, tree::apply_trees_to};
use crate::{
column::ColumnSample,
util::{RandomField, Sampler},
util::{FastNoise, RandomField, Sampler},
Canvas, IndexRef,
};
use common::{
@ -22,7 +22,7 @@ use rand::prelude::*;
use serde::Deserialize;
use std::{
f32,
ops::{Mul, Sub},
ops::{Mul, Range, Sub},
};
use vek::*;
@ -99,7 +99,7 @@ pub fn apply_paths_to(canvas: &mut Canvas) {
let head_space = path.head_space(path_dist);
for z in inset..inset + head_space {
let pos = Vec3::new(wpos2d.x, wpos2d.y, surface_z + z);
if canvas.get(pos).unwrap().kind() != BlockKind::Water {
if canvas.get(pos).kind() != BlockKind::Water {
let _ = canvas.set(pos, EMPTY_AIR);
}
}
@ -110,7 +110,7 @@ pub fn apply_paths_to(canvas: &mut Canvas) {
pub fn apply_caves_to(canvas: &mut Canvas, rng: &mut impl Rng) {
let info = canvas.info();
canvas.foreach_col(|canvas, wpos2d, col| {
let surface_z = col.riverless_alt.floor() as i32;
let surface_z = col.alt.floor() as i32;
if let Some((cave_dist, _, cave, _)) =
col.cave.filter(|(dist, _, cave, _)| *dist < cave.width)
@ -135,11 +135,7 @@ pub fn apply_caves_to(canvas: &mut Canvas, rng: &mut impl Rng) {
{
// If the block a little above is liquid, we should stop carving out the cave in
// order to leave a ceiling, and not floating water
if canvas
.get(Vec3::new(wpos2d.x, wpos2d.y, z + 2))
.map(|b| b.is_liquid())
.unwrap_or(false)
{
if canvas.get(Vec3::new(wpos2d.x, wpos2d.y, z + 2)).is_liquid() {
break;
}
@ -164,14 +160,20 @@ pub fn apply_caves_to(canvas: &mut Canvas, rng: &mut impl Rng) {
)
.mul(45.0) as i32;
for z in cave_roof - stalagtites..cave_roof {
canvas.set(
Vec3::new(wpos2d.x, wpos2d.y, z),
Block::new(
BlockKind::WeakRock,
info.index().colors.layer.stalagtite.into(),
),
);
// Generate stalagtites if there's something for them to hold on to
if canvas
.get(Vec3::new(wpos2d.x, wpos2d.y, cave_roof))
.is_filled()
{
for z in cave_roof - stalagtites..cave_roof {
canvas.set(
Vec3::new(wpos2d.x, wpos2d.y, z),
Block::new(
BlockKind::WeakRock,
info.index().colors.layer.stalagtite.into(),
),
);
}
}
let cave_depth = (col.alt - cave.alt).max(0.0);
@ -302,3 +304,78 @@ pub fn apply_caves_supplement<'a>(
}
}
}
#[allow(dead_code)]
pub fn apply_coral_to(canvas: &mut Canvas) {
let info = canvas.info();
if !info.chunk.river.near_water() {
return; // Don't bother with coral for a chunk nowhere near water
}
canvas.foreach_col(|canvas, wpos2d, col| {
const CORAL_DEPTH: Range<f32> = 14.0..32.0;
const CORAL_HEIGHT: f32 = 14.0;
const CORAL_DEPTH_FADEOUT: f32 = 5.0;
const CORAL_SCALE: f32 = 10.0;
let water_depth = col.water_level - col.alt;
if !CORAL_DEPTH.contains(&water_depth) {
return; // Avoid coral entirely for this column if we're outside coral depths
}
for z in col.alt.floor() as i32..(col.alt + CORAL_HEIGHT) as i32 {
let wpos = Vec3::new(wpos2d.x, wpos2d.y, z);
let coral_factor = Lerp::lerp(
1.0,
0.0,
// Fade coral out due to incorrect depth
((water_depth.clamped(CORAL_DEPTH.start, CORAL_DEPTH.end) - water_depth).abs()
/ CORAL_DEPTH_FADEOUT)
.min(1.0),
) * Lerp::lerp(
1.0,
0.0,
// Fade coral out due to incorrect altitude above the seabed
((z as f32 - col.alt) / CORAL_HEIGHT).powi(2),
) * FastNoise::new(info.index.seed + 7)
.get(wpos.map(|e| e as f64) / 32.0)
.sub(0.2)
.mul(100.0)
.clamped(0.0, 1.0);
let nz = Vec3::iota().map(|e: u32| FastNoise::new(info.index.seed + e * 177));
let wpos_warped = wpos.map(|e| e as f32)
+ nz.map(|nz| {
nz.get(wpos.map(|e| e as f64) / CORAL_SCALE as f64) * CORAL_SCALE * 0.3
});
// let is_coral = FastNoise2d::new(info.index.seed + 17)
// .get(wpos_warped.xy().map(|e| e as f64) / CORAL_SCALE)
// .sub(1.0 - coral_factor)
// .max(0.0)
// .div(coral_factor) > 0.5;
let is_coral = [
FastNoise::new(info.index.seed),
FastNoise::new(info.index.seed + 177),
]
.iter()
.all(|nz| {
nz.get(wpos_warped.map(|e| e as f64) / CORAL_SCALE as f64)
.abs()
< coral_factor * 0.3
});
if is_coral {
let _ = canvas.set(
wpos,
Block::new(BlockKind::WeakRock, Rgb::new(170, 220, 210)),
);
}
}
});
}

View File

@ -579,15 +579,13 @@ pub fn apply_scatter_to(canvas: &mut Canvas, rng: &mut impl Rng) {
.find(|z| {
canvas
.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))
.map(|b| b.is_solid())
.unwrap_or(false)
.is_solid()
})
.and_then(|solid_start| {
(1..8).map(|z| solid_start + z).find(|z| {
canvas
!canvas
.get(Vec3::new(wpos2d.x, wpos2d.y, alt + z))
.map(|b| !b.is_solid())
.unwrap_or(true)
.is_solid()
})
})
{

View File

@ -1,5 +1,5 @@
use crate::{
all::ForestKind,
all::*,
block::block_from_structure,
column::ColumnGen,
util::{RandomPerm, Sampler, UnitChooser},
@ -7,12 +7,16 @@ use crate::{
};
use common::{
assets::AssetHandle,
terrain::{Block, BlockKind, Structure, StructuresGroup},
terrain::{
structure::{Structure, StructureBlock, StructuresGroup},
Block, BlockKind, SpriteKind,
},
vol::ReadVol,
};
use hashbrown::HashMap;
use lazy_static::lazy_static;
use std::f32;
use rand::prelude::*;
use std::{f32, ops::Range};
use vek::*;
lazy_static! {
@ -36,10 +40,16 @@ static UNIT_CHOOSER: UnitChooser = UnitChooser::new(0x700F4EC7);
static QUIRKY_RAND: RandomPerm = RandomPerm::new(0xA634460F);
#[allow(clippy::if_same_then_else)]
pub fn apply_trees_to(canvas: &mut Canvas) {
pub fn apply_trees_to(canvas: &mut Canvas, dynamic_rng: &mut impl Rng) {
// TODO: Get rid of this
enum TreeModel {
Structure(Structure),
Procedural(ProceduralTree, StructureBlock),
}
struct Tree {
pos: Vec3<i32>,
model: Structure,
model: TreeModel,
seed: u32,
units: (Vec2<i32>, Vec2<i32>),
}
@ -48,11 +58,18 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
let info = canvas.info();
canvas.foreach_col(|canvas, wpos2d, col| {
let trees = info.land().get_near_trees(wpos2d);
let trees = info.chunks().get_near_trees(wpos2d);
for (tree_wpos, seed) in trees {
let tree = if let Some(tree) = tree_cache.entry(tree_wpos).or_insert_with(|| {
let col = ColumnGen::new(info.land()).get((tree_wpos, info.index()))?;
for TreeAttr {
pos,
seed,
scale,
forest_kind,
inhabited,
} in trees
{
let tree = if let Some(tree) = tree_cache.entry(pos).or_insert_with(|| {
let col = ColumnGen::new(info.chunks()).get((pos, info.index()))?;
let is_quirky = QUIRKY_RAND.chance(seed, 1.0 / 500.0);
@ -72,8 +89,8 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
}
Some(Tree {
pos: Vec3::new(tree_wpos.x, tree_wpos.y, col.alt as i32),
model: {
pos: Vec3::new(pos.x, pos.y, col.alt as i32),
model: 'model: {
let models: AssetHandle<_> = if is_quirky {
if col.temp > CONFIG.desert_temp {
*QUIRKY_DRY
@ -81,7 +98,7 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
*QUIRKY
}
} else {
match col.forest_kind {
match forest_kind {
ForestKind::Oak if QUIRKY_RAND.chance(seed + 1, 1.0 / 16.0) => {
*OAK_STUMPS
},
@ -91,17 +108,51 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
ForestKind::Palm => *PALMS,
ForestKind::Acacia => *ACACIAS,
ForestKind::Baobab => *BAOBABS,
ForestKind::Oak => *OAKS,
ForestKind::Pine => *PINES,
// ForestKind::Oak => *OAKS,
ForestKind::Oak => {
break 'model TreeModel::Procedural(
ProceduralTree::generate(
TreeConfig::oak(&mut RandomPerm::new(seed), scale),
&mut RandomPerm::new(seed),
),
StructureBlock::TemperateLeaves,
);
},
//ForestKind::Pine => *PINES,
ForestKind::Pine => {
break 'model TreeModel::Procedural(
ProceduralTree::generate(
TreeConfig::pine(&mut RandomPerm::new(seed), scale),
&mut RandomPerm::new(seed),
),
StructureBlock::PineLeaves,
);
},
ForestKind::Birch => *BIRCHES,
ForestKind::Mangrove => *MANGROVE_TREES,
ForestKind::Swamp => *SWAMP_TREES,
ForestKind::Giant => {
break 'model TreeModel::Procedural(
ProceduralTree::generate(
TreeConfig::giant(
&mut RandomPerm::new(seed),
scale,
inhabited,
),
&mut RandomPerm::new(seed),
),
StructureBlock::TemperateLeaves,
);
},
}
};
let models = models.read();
models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize % models.len()]
.clone()
TreeModel::Structure(
models[(MODEL_RAND.get(seed.wrapping_mul(17)) / 13) as usize
% models.len()]
.clone(),
)
},
seed,
units: UNIT_CHOOSER.get(seed),
@ -112,9 +163,22 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
continue;
};
let bounds = tree.model.get_bounds();
let bounds = match &tree.model {
TreeModel::Structure(s) => s.get_bounds(),
TreeModel::Procedural(t, _) => t.get_bounds().map(|e| e as i32),
};
let rpos2d = (wpos2d - tree.pos.xy())
.map2(Vec2::new(tree.units.0, tree.units.1), |p, unit| unit * p)
.sum();
if !Aabr::from(bounds).contains_point(rpos2d) {
// Skip this column
continue;
}
let mut is_top = true;
let mut is_leaf_top = true;
let mut last_block = Block::empty();
for z in (bounds.min.z..bounds.max.z).rev() {
let wpos = Vec3::new(wpos2d.x, wpos2d.y, tree.pos.z + z);
let model_pos = Vec3::from(
@ -127,11 +191,22 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
) + Vec3::unit_z() * (wpos.z - tree.pos.z);
block_from_structure(
info.index(),
if let Some(block) = tree.model.get(model_pos).ok().copied() {
if let Some(block) = match &tree.model {
TreeModel::Structure(s) => s.get(model_pos).ok().copied(),
TreeModel::Procedural(t, leaf_block) => Some(
match t.is_branch_or_leaves_at(model_pos.map(|e| e as f32 + 0.5)) {
(_, _, true, _) => {
StructureBlock::Block(BlockKind::Wood, Rgb::new(110, 68, 22))
},
(_, _, _, true) => StructureBlock::None,
(true, _, _, _) => StructureBlock::Log,
(_, true, _, _) => *leaf_block,
_ => StructureBlock::None,
},
),
} {
block
} else {
// If we hit an inaccessible block, we're probably outside the model bounds.
// Skip this column.
break;
},
wpos,
@ -141,8 +216,16 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
Block::air,
)
.map(|block| {
// Add a snow covering to the block above under certain circumstances
if col.snow_cover
// Add lights to the tree
if inhabited
&& last_block.is_air()
&& block.kind() == BlockKind::Wood
&& dynamic_rng.gen_range(0..256) == 0
{
canvas.set(wpos + Vec3::unit_z(), Block::air(SpriteKind::Lantern));
// Add a snow covering to the block above under certain
// circumstances
} else if col.snow_cover
&& ((block.kind() == BlockKind::Leaves && is_leaf_top)
|| (is_top && block.is_filled()))
{
@ -154,11 +237,420 @@ pub fn apply_trees_to(canvas: &mut Canvas) {
canvas.set(wpos, block);
is_leaf_top = false;
is_top = false;
last_block = block;
})
.unwrap_or_else(|| {
if last_block.kind() == BlockKind::Wood && dynamic_rng.gen_range(0..512) == 0 {
canvas.set(wpos, Block::air(SpriteKind::Beehive));
}
is_leaf_top = true;
last_block = Block::empty();
});
}
}
});
}
/// A type that specifies the generation properties of a tree.
pub struct TreeConfig {
/// Length of trunk, also scales other branches.
pub trunk_len: f32,
/// Radius of trunk, also scales other branches.
pub trunk_radius: f32,
// The scale that child branch lengths should be compared to their parents.
pub branch_child_len: f32,
// The scale that child branch radii should be compared to their parents.
pub branch_child_radius: f32,
/// The range of radii that leaf-emitting branches might have.
pub leaf_radius: Range<f32>,
/// 0 - 1 (0 = chaotic, 1 = straight).
pub straightness: f32,
/// Maximum number of branch layers (not including trunk).
pub max_depth: usize,
/// The number of branches that form from each branch.
pub splits: Range<f32>,
/// The range of proportions along a branch at which a split into another
/// branch might occur. This value is clamped between 0 and 1, but a
/// wider range may bias the results towards branch ends.
pub split_range: Range<f32>,
/// The bias applied to the length of branches based on the proportion along
/// their parent that they eminate from. -1.0 = negative bias (branches
/// at ends are longer, branches at the start are shorter) 0.0 = no bias
/// (branches do not change their length with regard to parent branch
/// proportion) 1.0 = positive bias (branches at ends are shorter,
/// branches at the start are longer)
pub branch_len_bias: f32,
/// The scale of leaves in the vertical plane. Less than 1.0 implies a
/// flattening of the leaves.
pub leaf_vertical_scale: f32,
/// How evenly spaced (vs random) sub-branches are along their parent.
pub proportionality: f32,
/// Whether the tree is inhabited (adds various features and effects)
pub inhabited: bool,
}
impl TreeConfig {
pub fn oak(rng: &mut impl Rng, scale: f32) -> Self {
let scale = scale * (0.8 + rng.gen::<f32>().powi(4) * 0.75);
let log_scale = 1.0 + scale.log2().max(0.0);
Self {
trunk_len: 9.0 * scale,
trunk_radius: 2.0 * scale,
branch_child_len: 0.9,
branch_child_radius: 0.75,
leaf_radius: 2.5 * log_scale..3.25 * log_scale,
straightness: 0.45,
max_depth: 4,
splits: 2.25..3.25,
split_range: 0.75..1.5,
branch_len_bias: 0.0,
leaf_vertical_scale: 1.0,
proportionality: 0.0,
inhabited: false,
}
}
pub fn pine(rng: &mut impl Rng, scale: f32) -> Self {
let scale = scale * (1.0 + rng.gen::<f32>().powi(4) * 0.5);
let log_scale = 1.0 + scale.log2().max(0.0);
Self {
trunk_len: 32.0 * scale,
trunk_radius: 1.25 * scale,
branch_child_len: 0.35 / scale,
branch_child_radius: 0.0,
leaf_radius: 2.5 * log_scale..2.75 * log_scale,
straightness: 0.0,
max_depth: 1,
splits: 40.0..50.0,
split_range: 0.165..1.2,
branch_len_bias: 0.75,
leaf_vertical_scale: 0.3,
proportionality: 1.0,
inhabited: false,
}
}
pub fn giant(_rng: &mut impl Rng, scale: f32, inhabited: bool) -> Self {
let log_scale = 1.0 + scale.log2().max(0.0);
Self {
trunk_len: 11.0 * scale,
trunk_radius: 6.0 * scale,
branch_child_len: 0.9,
branch_child_radius: 0.75,
leaf_radius: 2.5 * scale..3.75 * scale,
straightness: 0.36,
max_depth: (7.0 + log_scale) as usize,
splits: 1.5..2.5,
split_range: 1.0..1.1,
branch_len_bias: 0.0,
leaf_vertical_scale: 0.6,
proportionality: 0.0,
inhabited,
}
}
}
// TODO: Rename this to `Tree` when the name conflict is gone
pub struct ProceduralTree {
branches: Vec<Branch>,
trunk_idx: usize,
}
impl ProceduralTree {
/// Generate a new tree using the given configuration and seed.
pub fn generate(config: TreeConfig, rng: &mut impl Rng) -> Self {
let mut this = Self {
branches: Vec::new(),
trunk_idx: 0, // Gets replaced later
};
// Add the tree trunk (and sub-branches) recursively
let (trunk_idx, _) = this.add_branch(
&config,
// Our trunk starts at the origin...
Vec3::zero(),
// ...and has a roughly upward direction
Vec3::new(rng.gen_range(-1.0..1.0), rng.gen_range(-1.0..1.0), 10.0).normalized(),
config.trunk_len,
config.trunk_radius,
0,
None,
rng,
);
this.trunk_idx = trunk_idx;
this
}
// Recursively add a branch (with sub-branches) to the tree's branch graph,
// returning the index and AABB of the branch. This AABB gets propagated
// down to the parent and is used later during sampling to cull the branches to
// be sampled.
#[allow(clippy::too_many_arguments)]
fn add_branch(
&mut self,
config: &TreeConfig,
start: Vec3<f32>,
dir: Vec3<f32>,
branch_len: f32,
branch_radius: f32,
depth: usize,
sibling_idx: Option<usize>,
rng: &mut impl Rng,
) -> (usize, Aabb<f32>) {
let end = start + dir * branch_len;
let line = LineSegment3 { start, end };
let wood_radius = branch_radius;
let leaf_radius = if depth == config.max_depth {
rng.gen_range(config.leaf_radius.clone())
} else {
0.0
};
let has_stairs = config.inhabited
&& depth < config.max_depth
&& branch_radius > 6.5
&& start.xy().distance(end.xy()) < (start.z - end.z).abs() * 1.5;
let bark_radius = if has_stairs { 5.0 } else { 0.0 } + wood_radius * 0.25;
// The AABB that covers this branch, along with wood and leaves that eminate
// from it
let mut aabb = Aabb {
min: Vec3::partial_min(start, end) - (wood_radius + bark_radius).max(leaf_radius),
max: Vec3::partial_max(start, end) + (wood_radius + bark_radius).max(leaf_radius),
};
let mut child_idx = None;
// Don't add child branches if we're already enough layers into the tree
if depth < config.max_depth {
let x_axis = dir
.cross(Vec3::<f32>::zero().map(|_| rng.gen_range(-1.0..1.0)))
.normalized();
let y_axis = dir.cross(x_axis).normalized();
let screw_shift = rng.gen_range(0.0..f32::consts::TAU);
let splits = rng.gen_range(config.splits.clone()).round() as usize;
for i in 0..splits {
let dist = Lerp::lerp(
rng.gen_range(0.0..1.0),
i as f32 / (splits - 1) as f32,
config.proportionality,
);
const PHI: f32 = 0.618;
const RAD_PER_BRANCH: f32 = f32::consts::TAU * PHI;
let screw = (screw_shift + i as f32 * RAD_PER_BRANCH).sin() * x_axis
+ (screw_shift + i as f32 * RAD_PER_BRANCH).cos() * y_axis;
// Choose a point close to the branch to act as the target direction for the
// branch to grow in let split_factor =
// rng.gen_range(config.split_range.start, config.split_range.end).clamped(0.0,
// 1.0);
let split_factor =
Lerp::lerp(config.split_range.start, config.split_range.end, dist);
let tgt = Lerp::lerp_unclamped(start, end, split_factor)
+ Lerp::lerp(
Vec3::<f32>::zero().map(|_| rng.gen_range(-1.0..1.0)),
screw,
config.proportionality,
);
// Start the branch at the closest point to the target
let branch_start = line.projected_point(tgt);
// Now, interpolate between the target direction and the parent branch's
// direction to find a direction
let branch_dir =
Lerp::lerp(tgt - branch_start, dir, config.straightness).normalized();
let (branch_idx, branch_aabb) = self.add_branch(
config,
branch_start,
branch_dir,
branch_len
* config.branch_child_len
* (1.0
- (split_factor - 0.5)
* 2.0
* config.branch_len_bias.clamped(-1.0, 1.0)),
branch_radius * config.branch_child_radius,
depth + 1,
child_idx,
rng,
);
child_idx = Some(branch_idx);
// Parent branches AABBs include the AABBs of child branches to allow for
// culling during sampling
aabb.expand_to_contain(branch_aabb);
}
}
let idx = self.branches.len(); // Compute the index that this branch is going to have
self.branches.push(Branch {
line,
wood_radius,
leaf_radius,
leaf_vertical_scale: config.leaf_vertical_scale,
aabb,
sibling_idx,
child_idx,
has_stairs,
});
(idx, aabb)
}
/// Get the bounding box that covers the tree (all branches and leaves)
pub fn get_bounds(&self) -> Aabb<f32> { self.branches[self.trunk_idx].aabb }
// Recursively search for branches or leaves by walking the tree's branch graph.
fn is_branch_or_leaves_at_inner(
&self,
pos: Vec3<f32>,
parent: &Branch,
branch_idx: usize,
) -> (bool, bool, bool, bool) {
let branch = &self.branches[branch_idx];
// Always probe the sibling branch, since our AABB doesn't include its bounds
// (it's not one of our children)
let branch_or_leaves = branch
.sibling_idx
.map(|idx| Vec4::<bool>::from(self.is_branch_or_leaves_at_inner(pos, parent, idx)))
.unwrap_or_default();
// Only continue probing this sub-graph of the tree if the sample position falls
// within its AABB
if branch.aabb.contains_point(pos) {
// Probe this branch
let (this, _d2) = branch.is_branch_or_leaves_at(pos, parent);
let siblings = branch_or_leaves | Vec4::from(this);
// Probe the children of this branch
let children = branch
.child_idx
.map(|idx| Vec4::<bool>::from(self.is_branch_or_leaves_at_inner(pos, branch, idx)))
.unwrap_or_default();
// Only allow empties for children if there is no solid at the current depth
(siblings | children).into_tuple()
} else {
branch_or_leaves.into_tuple()
}
}
/// Determine whether there are either branches or leaves at the given
/// position in the tree.
#[inline(always)]
pub fn is_branch_or_leaves_at(&self, pos: Vec3<f32>) -> (bool, bool, bool, bool) {
let (log, leaf, platform, air) =
self.is_branch_or_leaves_at_inner(pos, &self.branches[self.trunk_idx], self.trunk_idx);
(log /* & !air */, leaf & !air, platform & !air, air)
}
}
// Branches are arranged in a graph shape. Each branch points to both its first
// child (if any) and also to the next branch in the list of child branches
// associated with the parent. This means that the entire tree is laid out in a
// walkable graph where each branch refers only to two other branches. As a
// result, walking the tree is simply a case of performing double recursion.
struct Branch {
line: LineSegment3<f32>,
wood_radius: f32,
leaf_radius: f32,
leaf_vertical_scale: f32,
aabb: Aabb<f32>,
sibling_idx: Option<usize>,
child_idx: Option<usize>,
has_stairs: bool,
}
impl Branch {
/// Determine whether there are either branches or leaves at the given
/// position in the branch.
/// (branch, leaves, stairs, forced_air)
pub fn is_branch_or_leaves_at(
&self,
pos: Vec3<f32>,
parent: &Branch,
) -> ((bool, bool, bool, bool), f32) {
// fn finvsqrt(x: f32) -> f32 {
// let y = f32::from_bits(0x5f375a86 - (x.to_bits() >> 1));
// y * (1.5 - ( x * 0.5 * y * y ))
// }
fn length_factor(line: LineSegment3<f32>, p: Vec3<f32>) -> f32 {
let len_sq = line.start.distance_squared(line.end);
if len_sq < 0.001 {
0.0
} else {
(p - line.start).dot(line.end - line.start) / len_sq
}
}
// fn smooth(a: f32, b: f32, k: f32) -> f32 {
// // let h = (0.5 + 0.5 * (b - a) / k).clamped(0.0, 1.0);
// // Lerp::lerp(b, a, h) - k * h * (1.0 - h)
// let h = (k-(a-b).abs()).max(0.0);
// a.min(b) - h * h * 0.25 / k
// }
let p = self.line.projected_point(pos);
let d2 = p.distance_squared(pos);
let length_factor = length_factor(self.line, pos);
let wood_radius = Lerp::lerp(parent.wood_radius, self.wood_radius, length_factor);
let mask = if d2 < wood_radius.powi(2) {
(true, false, false, false) // Wood
} else if {
let diff = (p - pos) / Vec3::new(1.0, 1.0, self.leaf_vertical_scale);
diff.magnitude_squared() < self.leaf_radius.powi(2)
} {
(false, true, false, false) // Leaves
} else {
let stair_width = 5.0;
let stair_thickness = 2.0;
let stair_space = 5.0;
if self.has_stairs {
let (platform, air) = if pos.z >= self.line.start.z.min(self.line.end.z) - 1.0
&& pos.z
<= self.line.start.z.max(self.line.end.z) + stair_thickness + stair_space
&& d2 < (wood_radius + stair_width).powi(2)
{
let rpos = pos.xy() - p;
let stretch = 32.0;
let stair_section =
((rpos.x as f32).atan2(rpos.y as f32) / (f32::consts::PI * 2.0) * stretch
+ pos.z)
.rem_euclid(stretch);
(
stair_section < stair_thickness,
stair_section >= stair_thickness
&& stair_section < stair_thickness + stair_space,
) // Stairs
} else {
(false, false)
};
let platform = platform
|| (self.has_stairs
&& self.wood_radius > 4.0
&& !air
&& d2 < (wood_radius + 10.0).powi(2)
&& pos.z % 48.0 < stair_thickness);
(false, false, platform, air)
} else {
(false, false, false, false)
}
};
(mask, d2)
}
}

View File

@ -1,6 +1,10 @@
#![deny(unsafe_code)]
#![allow(incomplete_features)]
#![allow(clippy::option_map_unit_fn)]
#![allow(
clippy::option_map_unit_fn,
clippy::blocks_in_if_conditions,
clippy::too_many_arguments
)]
#![deny(clippy::clone_on_ref_ptr)]
#![feature(
arbitrary_enum_discriminant,
@ -9,7 +13,8 @@
const_panic,
label_break_value,
or_patterns,
array_value_iter
array_value_iter,
array_map
)]
mod all;
@ -19,17 +24,20 @@ pub mod civ;
mod column;
pub mod config;
pub mod index;
pub mod land;
pub mod layer;
pub mod pathfinding;
pub mod sim;
pub mod sim2;
pub mod site;
pub mod site2;
pub mod util;
// Reexports
pub use crate::{
canvas::{Canvas, CanvasInfo},
config::CONFIG,
land::Land,
};
pub use block::BlockGen;
pub use column::ColumnSample;
@ -117,6 +125,8 @@ impl World {
},
},
civ::SiteKind::Castle => world_msg::SiteKind::Castle,
civ::SiteKind::Refactor => world_msg::SiteKind::Town,
civ::SiteKind::Tree => world_msg::SiteKind::Tree,
},
wpos: site.center * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
}
@ -278,22 +288,24 @@ impl World {
wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
column_grid: &zcache_grid,
column_grid_border: grid_border,
land: &self.sim,
chunks: &self.sim,
index,
chunk: sim_chunk,
},
chunk: &mut chunk,
};
layer::apply_trees_to(&mut canvas);
layer::apply_scatter_to(&mut canvas, &mut dynamic_rng);
layer::apply_caves_to(&mut canvas, &mut dynamic_rng);
layer::apply_trees_to(&mut canvas, &mut dynamic_rng);
layer::apply_scatter_to(&mut canvas, &mut dynamic_rng);
layer::apply_paths_to(&mut canvas);
// layer::apply_coral_to(&mut canvas);
// Apply site generation
sim_chunk.sites.iter().for_each(|site| {
index.sites[*site].apply_to(index, chunk_wpos2d, sample_get, &mut chunk)
});
sim_chunk
.sites
.iter()
.for_each(|site| index.sites[*site].apply_to(&mut canvas, &mut dynamic_rng));
let gen_entity_pos = |dynamic_rng: &mut rand::rngs::ThreadRng| {
let lpos2d = TerrainChunkSize::RECT_SIZE

View File

@ -133,7 +133,7 @@ pub fn sample_pos(
let humidity = humidity.min(1.0).max(0.0);
let temperature = temperature.min(1.0).max(-1.0) * 0.5 + 0.5;
let wpos = pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let column_rgb = samples
let column_rgb_alt = samples
.and_then(|samples| {
chunk_idx
.and_then(|chunk_idx| samples.get(chunk_idx))
@ -146,7 +146,7 @@ pub fn sample_pos(
let basement = sample.basement;
let grass_depth = (1.5 + 2.0 * sample.chaos).min(alt - basement);
let wposz = if is_basement { basement } else { alt };
if is_basement && wposz < alt - grass_depth {
let rgb = if is_basement && wposz < alt - grass_depth {
Lerp::lerp(
sample.sub_surface_color,
sample.stone_col.map(|e| e as f32 / 255.0),
@ -160,11 +160,17 @@ pub fn sample_pos(
((wposz as f32 - (alt - grass_depth)) / grass_depth).sqrt(),
)
.map(|e| e as f64)
}
};
(rgb, alt)
});
let downhill_wpos = downhill.unwrap_or(wpos + TerrainChunkSize::RECT_SIZE.map(|e| e as i32));
let alt = if is_basement { basement } else { alt };
let alt = if is_basement {
basement
} else {
column_rgb_alt.map_or(alt, |(_, alt)| alt)
};
let true_water_alt = (alt.max(water_alt) as f64 - focus.z) / gain as f64;
let true_alt = (alt as f64 - focus.z) / gain as f64;
@ -183,7 +189,7 @@ pub fn sample_pos(
if is_shaded { 1.0 } else { alt },
if is_shaded || is_humidity { 1.0 } else { 0.0 },
);
let column_rgb = column_rgb.unwrap_or(default_rgb);
let column_rgb = column_rgb_alt.map(|(rgb, _)| rgb).unwrap_or(default_rgb);
let mut connections = [None; 8];
let mut has_connections = false;
// TODO: Support non-river connections.

View File

@ -23,20 +23,22 @@ pub use self::{
};
use crate::{
all::ForestKind,
all::{Environment, ForestKind, TreeAttr},
block::BlockGen,
civ::Place,
column::ColumnGen,
site::Site,
util::{
seed_expan, FastNoise, RandomField, RandomPerm, Sampler, StructureGen2d, LOCALITY,
NEIGHBORS,
seed_expan, DHashSet, FastNoise, FastNoise2d, RandomField, Sampler, StructureGen2d,
CARDINALS, LOCALITY, NEIGHBORS,
},
IndexRef, CONFIG,
};
use common::{
assets::{self, AssetExt},
grid::Grid,
lottery::Lottery,
spiral::Spiral2d,
store::Id,
terrain::{
map::MapConfig, uniform_idx_as_vec2, vec2_as_uniform_idx, BiomeKind, MapSizeLg,
@ -45,6 +47,7 @@ use common::{
vol::RectVolSize,
};
use common_net::msg::WorldMapMsg;
use enum_iterator::IntoEnumIterator;
use noise::{
BasicMulti, Billow, Fbm, HybridMulti, MultiFractal, NoiseFn, RangeFunction, RidgedMulti,
Seedable, SuperSimplex, Worley,
@ -114,6 +117,7 @@ pub(crate) struct GenCtx {
pub cave_1_nz: SuperSimplex,
pub structure_gen: StructureGen2d,
pub big_structure_gen: StructureGen2d,
pub region_gen: StructureGen2d,
pub fast_turb_x_nz: FastNoise,
@ -516,7 +520,8 @@ impl WorldSim {
cave_0_nz: SuperSimplex::new().set_seed(rng.gen()),
cave_1_nz: SuperSimplex::new().set_seed(rng.gen()),
structure_gen: StructureGen2d::new(rng.gen(), 32, 16),
structure_gen: StructureGen2d::new(rng.gen(), 24, 10),
big_structure_gen: StructureGen2d::new(rng.gen(), 768, 512),
region_gen: StructureGen2d::new(rng.gen(), 400, 96),
humid_nz: Billow::new()
.set_octaves(9)
@ -1397,6 +1402,8 @@ impl WorldSim {
rng,
};
this.generate_cliffs();
if opts.seed_elements {
this.seed_elements();
}
@ -1509,6 +1516,50 @@ impl WorldSim {
}
}
pub fn generate_cliffs(&mut self) {
let mut rng = self.rng.clone();
for _ in 0..self.get_size().product() / 10 {
let mut pos = self.get_size().map(|e| rng.gen_range(0..e) as i32);
let mut cliffs = DHashSet::default();
let mut cliff_path = Vec::new();
for _ in 0..64 {
if self.get_gradient_approx(pos).map_or(false, |g| g > 1.5) {
if !cliffs.insert(pos) {
break;
}
cliff_path.push((pos, 0.0));
pos += CARDINALS
.iter()
.copied()
.max_by_key(|rpos| {
self.get_gradient_approx(pos + rpos)
.map_or(0, |g| (g * 1000.0) as i32)
})
.unwrap(); // Can't fail
} else {
break;
}
}
for cliff in cliffs {
Spiral2d::new()
.take((4usize * 2 + 1).pow(2))
.for_each(|rpos| {
let dist = rpos.map(|e| e as f32).magnitude();
if let Some(c) = self.get_mut(cliff + rpos) {
let warp = 1.0 / (1.0 + dist);
c.tree_density *= 1.0 - warp;
c.cliff_height = Lerp::lerp(44.0, 0.0, -1.0 + dist / 3.5);
}
});
}
}
}
/// Prepare the world for simulation
pub fn seed_elements(&mut self) {
let mut rng = self.rng.clone();
@ -1995,9 +2046,58 @@ impl WorldSim {
/// Return an iterator over candidate tree positions (note that only some of
/// these will become trees since environmental parameters may forbid
/// them spawning).
pub fn get_near_trees(&self, wpos: Vec2<i32>) -> impl Iterator<Item = (Vec2<i32>, u32)> + '_ {
pub fn get_near_trees(&self, wpos: Vec2<i32>) -> impl Iterator<Item = TreeAttr> + '_ {
// Deterministic based on wpos
std::array::IntoIter::new(self.gen_ctx.structure_gen.get(wpos))
let normal_trees = std::array::IntoIter::new(self.gen_ctx.structure_gen.get(wpos))
.filter_map(move |(pos, seed)| {
let chunk = self.get_wpos(pos)?;
let env = Environment {
humid: chunk.humidity,
temp: chunk.temp,
near_water: if chunk.river.is_lake() || chunk.river.near_river() {
1.0
} else {
0.0
},
};
Some(TreeAttr {
pos,
seed,
scale: 1.0,
forest_kind: *Lottery::from(
ForestKind::into_enum_iter()
.enumerate()
.map(|(i, fk)| {
const CLUSTER_SIZE: f64 = 48.0;
let nz = (FastNoise2d::new(i as u32 * 37)
.get(pos.map(|e| e as f64) / CLUSTER_SIZE)
+ 1.0)
/ 2.0;
(fk.proclivity(&env) * nz, Some(fk))
})
.chain(std::iter::once((0.001, None)))
.collect::<Vec<_>>(),
)
.choose_seeded(seed)
.as_ref()?,
inhabited: false,
})
});
// // For testing
// let giant_trees =
// std::array::IntoIter::new(self.gen_ctx.big_structure_gen.get(wpos))
// // Don't even consider trees if we aren't close
// .filter(move |(pos, _)| pos.distance_squared(wpos) < 512i32.pow(2))
// .map(move |(pos, seed)| TreeAttr {
// pos,
// seed,
// scale: 5.0,
// forest_kind: ForestKind::Giant,
// inhabited: (seed / 13) % 2 == 0,
// });
normal_trees //.chain(giant_trees)
}
}
@ -2016,7 +2116,6 @@ pub struct SimChunk {
pub forest_kind: ForestKind,
pub spawn_rate: f32,
pub river: RiverData,
pub warp_factor: f32,
pub surface_veg: f32,
pub sites: Vec<Id<Site>>,
@ -2024,6 +2123,7 @@ pub struct SimChunk {
pub path: (Way, Path),
pub cave: (Way, Cave),
pub cliff_height: f32,
pub contains_waypoint: bool,
}
@ -2232,91 +2332,30 @@ impl SimChunk {
},
tree_density,
forest_kind: {
// Whittaker diagram
let candidates = [
// A smaller prevalence means that the range of values this tree appears in
// will shrink compared to neighbouring trees in the
// topology of the Whittaker diagram.
// Humidity, temperature, near_water, each with prevalence
(
ForestKind::Palm,
(CONFIG.desert_hum, 1.5),
((CONFIG.tropical_temp + CONFIG.desert_temp) / 2.0, 1.25),
(1.0, 2.0),
),
(
ForestKind::Acacia,
(0.0, 1.5),
(CONFIG.tropical_temp, 1.5),
(0.0, 1.0),
),
(
ForestKind::Baobab,
(0.0, 1.5),
(CONFIG.tropical_temp, 1.5),
(0.0, 1.0),
),
(
ForestKind::Oak,
(CONFIG.forest_hum, 1.5),
(0.0, 1.5),
(0.0, 1.0),
),
(
ForestKind::Mangrove,
(CONFIG.jungle_hum, 0.5),
(CONFIG.tropical_temp, 0.5),
(0.0, 1.0),
),
(
ForestKind::Pine,
(CONFIG.forest_hum, 1.25),
(CONFIG.snow_temp, 2.5),
(0.0, 1.0),
),
(
ForestKind::Birch,
(CONFIG.desert_hum, 1.5),
(CONFIG.temperate_temp, 1.5),
(0.0, 1.0),
),
(
ForestKind::Swamp,
((CONFIG.forest_hum + CONFIG.jungle_hum) / 2.0, 2.0),
((CONFIG.temperate_temp + CONFIG.snow_temp) / 2.0, 2.0),
(1.0, 2.5),
),
];
let env = Environment {
humid: humidity,
temp,
near_water: if river.is_lake() || river.near_river() {
1.0
} else {
0.0
},
};
candidates
.iter()
.enumerate()
.min_by_key(|(i, (_, (h, h_prev), (t, t_prev), (w, w_prev)))| {
let rand = RandomPerm::new(*i as u32 * 1000);
let noise =
Vec3::iota().map(|e| (rand.get(e) & 0xFF) as f32 / 255.0 - 0.5) * 2.0;
(Vec3::new(
(*h - humidity) / *h_prev,
(*t - temp) / *t_prev,
(*w - if river.near_water() { 1.0 } else { 0.0 }) / *w_prev,
)
.add(noise * 0.1)
.map(|e| e * e)
.sum()
* 10000.0) as i32
})
.map(|(_, c)| c.0)
ForestKind::into_enum_iter()
.max_by_key(|fk| (fk.proclivity(&env) * 10000.0) as u32)
.unwrap() // Can't fail
},
spawn_rate: 1.0,
river,
warp_factor: 1.0,
surface_veg: 1.0,
sites: Vec::new(),
place: None,
path: Default::default(),
cave: Default::default(),
cliff_height: 0.0,
contains_waypoint: false,
}
}
@ -2348,4 +2387,6 @@ impl SimChunk {
BiomeKind::Grassland
}
}
pub fn near_cliffs(&self) -> bool { self.cliff_height > 0.0 }
}

View File

@ -88,7 +88,8 @@ pub fn simulate(index: &mut Index, world: &mut WorldSim) {
}
pub fn tick(index: &mut Index, _world: &mut WorldSim, dt: f32) {
for site in index.sites.ids() {
let site_ids = index.sites.ids().collect::<Vec<_>>();
for site in site_ids {
tick_site_economy(index, site, dt);
}

View File

@ -4,19 +4,16 @@ mod dungeon;
pub mod economy;
pub mod namegen;
mod settlement;
mod tree;
// Reexports
pub use self::{
block_mask::BlockMask, castle::Castle, dungeon::Dungeon, economy::Economy,
settlement::Settlement,
settlement::Settlement, tree::Tree,
};
use crate::{column::ColumnSample, IndexRef};
use common::{
generation::ChunkSupplement,
terrain::Block,
vol::{BaseVol, ReadVol, RectSizedVol, WriteVol},
};
use crate::{column::ColumnSample, site2, Canvas};
use common::generation::ChunkSupplement;
use rand::Rng;
use serde::Deserialize;
use vek::*;
@ -45,6 +42,8 @@ pub enum SiteKind {
Settlement(Settlement),
Dungeon(Dungeon),
Castle(Castle),
Refactor(site2::Site),
Tree(tree::Tree),
}
impl Site {
@ -69,11 +68,27 @@ impl Site {
}
}
pub fn refactor(s: site2::Site) -> Self {
Self {
kind: SiteKind::Refactor(s),
economy: Economy::default(),
}
}
pub fn tree(t: tree::Tree) -> Self {
Self {
kind: SiteKind::Tree(t),
economy: Economy::default(),
}
}
pub fn radius(&self) -> f32 {
match &self.kind {
SiteKind::Settlement(s) => s.radius(),
SiteKind::Dungeon(d) => d.radius(),
SiteKind::Castle(c) => c.radius(),
SiteKind::Refactor(s) => s.radius(),
SiteKind::Tree(t) => t.radius(),
}
}
@ -82,6 +97,8 @@ impl Site {
SiteKind::Settlement(s) => s.get_origin(),
SiteKind::Dungeon(d) => d.get_origin(),
SiteKind::Castle(c) => c.get_origin(),
SiteKind::Refactor(s) => s.origin,
SiteKind::Tree(t) => t.origin,
}
}
@ -90,6 +107,8 @@ impl Site {
SiteKind::Settlement(s) => s.spawn_rules(wpos),
SiteKind::Dungeon(d) => d.spawn_rules(wpos),
SiteKind::Castle(c) => c.spawn_rules(wpos),
SiteKind::Refactor(s) => s.spawn_rules(wpos),
SiteKind::Tree(t) => t.spawn_rules(wpos),
}
}
@ -98,20 +117,20 @@ impl Site {
SiteKind::Settlement(s) => s.name(),
SiteKind::Dungeon(d) => d.name(),
SiteKind::Castle(c) => c.name(),
SiteKind::Refactor(_) => "Town",
SiteKind::Tree(_) => "Giant Tree",
}
}
pub fn apply_to<'a>(
&'a self,
index: IndexRef,
wpos2d: Vec2<i32>,
get_column: impl FnMut(Vec2<i32>) -> Option<&'a ColumnSample<'a>>,
vol: &mut (impl BaseVol<Vox = Block> + RectSizedVol + ReadVol + WriteVol),
) {
pub fn apply_to<'a>(&'a self, canvas: &mut Canvas, dynamic_rng: &mut impl Rng) {
let info = canvas.info();
let get_col = |wpos| info.col(wpos + info.wpos);
match &self.kind {
SiteKind::Settlement(s) => s.apply_to(index, wpos2d, get_column, vol),
SiteKind::Dungeon(d) => d.apply_to(index, wpos2d, get_column, vol),
SiteKind::Castle(c) => c.apply_to(index, wpos2d, get_column, vol),
SiteKind::Settlement(s) => s.apply_to(canvas.index, canvas.wpos, get_col, canvas.chunk),
SiteKind::Dungeon(d) => d.apply_to(canvas.index, canvas.wpos, get_col, canvas.chunk),
SiteKind::Castle(c) => c.apply_to(canvas.index, canvas.wpos, get_col, canvas.chunk),
SiteKind::Refactor(s) => s.render(canvas, dynamic_rng),
SiteKind::Tree(t) => t.render(canvas, dynamic_rng),
}
}
@ -129,6 +148,8 @@ impl Site {
},
SiteKind::Dungeon(d) => d.apply_supplement(dynamic_rng, wpos2d, get_column, supplement),
SiteKind::Castle(c) => c.apply_supplement(dynamic_rng, wpos2d, get_column, supplement),
SiteKind::Refactor(_) => {},
SiteKind::Tree(_) => {},
}
}
}

89
world/src/site/tree.rs Normal file
View File

@ -0,0 +1,89 @@
use crate::{
layer::tree::{ProceduralTree, TreeConfig},
site::SpawnRules,
util::{FastNoise, Sampler},
Canvas, Land,
};
use common::terrain::{Block, BlockKind};
use rand::prelude::*;
use vek::*;
// Temporary, do trees through the new site system later
pub struct Tree {
pub origin: Vec2<i32>,
alt: i32,
seed: u32,
tree: ProceduralTree,
}
impl Tree {
pub fn generate(origin: Vec2<i32>, land: &Land, rng: &mut impl Rng) -> Self {
Self {
origin,
alt: land.get_alt_approx(origin) as i32,
seed: rng.gen(),
tree: {
let config = TreeConfig::giant(rng, 4.0, false);
ProceduralTree::generate(config, rng)
},
}
}
pub fn radius(&self) -> f32 { 512.0 }
pub fn spawn_rules(&self, wpos: Vec2<i32>) -> SpawnRules {
let trunk_radius = 48i32;
SpawnRules {
trees: wpos.distance_squared(self.origin) > trunk_radius.pow(2),
}
}
pub fn render(&self, canvas: &mut Canvas, _dynamic_rng: &mut impl Rng) {
let nz = FastNoise::new(self.seed);
canvas.foreach_col(|canvas, wpos2d, col| {
let rpos2d = wpos2d - self.origin;
let bounds = self.tree.get_bounds().map(|e| e as i32);
if !Aabr::from(bounds).contains_point(rpos2d) {
// Skip this column
return;
}
let mut above = true;
for z in (bounds.min.z..bounds.max.z + 1).rev() {
let wpos = wpos2d.with_z(self.alt + z);
let rposf = (wpos - self.origin.with_z(self.alt)).map(|e| e as f32 + 0.5);
let (branch, leaves, _, _) = self.tree.is_branch_or_leaves_at(rposf);
if branch || leaves {
if above && col.snow_cover {
canvas.set(
wpos + Vec3::unit_z(),
Block::new(BlockKind::Snow, Rgb::new(255, 255, 255)),
);
}
if leaves {
let dark = Rgb::new(10, 70, 50).map(|e| e as f32);
let light = Rgb::new(80, 140, 10).map(|e| e as f32);
let leaf_col = Lerp::lerp(
dark,
light,
nz.get(rposf.map(|e| e as f64) * 0.05) * 0.5 + 0.5,
);
canvas.set(
wpos,
Block::new(BlockKind::Leaves, leaf_col.map(|e| e as u8)),
);
} else if branch {
canvas.set(wpos, Block::new(BlockKind::Wood, Rgb::new(80, 32, 0)));
}
above = false;
}
}
});
}
}

135
world/src/site2/gen.rs Normal file
View File

@ -0,0 +1,135 @@
use super::*;
use crate::util::{RandomField, Sampler};
use common::{
store::{Id, Store},
terrain::{Block, BlockKind},
};
use vek::*;
pub enum Primitive {
Empty, // Placeholder
// Shapes
Aabb(Aabb<i32>),
Pyramid { aabb: Aabb<i32>, inset: i32 },
// Combinators
And(Id<Primitive>, Id<Primitive>),
Or(Id<Primitive>, Id<Primitive>),
Xor(Id<Primitive>, Id<Primitive>),
}
pub enum Fill {
Block(Block),
Brick(BlockKind, Rgb<u8>, u8),
}
impl Fill {
fn contains_at(&self, tree: &Store<Primitive>, prim: Id<Primitive>, pos: Vec3<i32>) -> bool {
// Custom closure because vek's impl of `contains_point` is inclusive :(
let aabb_contains = |aabb: Aabb<i32>, pos: Vec3<i32>| {
(aabb.min.x..aabb.max.x).contains(&pos.x)
&& (aabb.min.y..aabb.max.y).contains(&pos.y)
&& (aabb.min.z..aabb.max.z).contains(&pos.z)
};
match &tree[prim] {
Primitive::Empty => false,
Primitive::Aabb(aabb) => aabb_contains(*aabb, pos),
Primitive::Pyramid { aabb, inset } => {
let inset = (*inset).max(aabb.size().reduce_min());
let inner = Aabr {
min: aabb.min.xy() - 1 + inset,
max: aabb.max.xy() - inset,
};
aabb_contains(*aabb, pos)
&& (inner.projected_point(pos.xy()) - pos.xy())
.map(|e| e.abs())
.reduce_max() as f32
/ (inset as f32)
< 1.0
- ((pos.z - aabb.min.z) as f32 + 0.5) / (aabb.max.z - aabb.min.z) as f32
},
Primitive::And(a, b) => {
self.contains_at(tree, *a, pos) && self.contains_at(tree, *b, pos)
},
Primitive::Or(a, b) => {
self.contains_at(tree, *a, pos) || self.contains_at(tree, *b, pos)
},
Primitive::Xor(a, b) => {
self.contains_at(tree, *a, pos) ^ self.contains_at(tree, *b, pos)
},
}
}
pub fn sample_at(
&self,
tree: &Store<Primitive>,
prim: Id<Primitive>,
pos: Vec3<i32>,
) -> Option<Block> {
if self.contains_at(tree, prim, pos) {
match self {
Fill::Block(block) => Some(*block),
Fill::Brick(bk, col, range) => Some(Block::new(
*bk,
*col + (RandomField::new(13)
.get((pos + Vec3::new(pos.z, pos.z, 0)) / Vec3::new(2, 2, 1))
% *range as u32) as u8,
)),
}
} else {
None
}
}
fn get_bounds_inner(&self, tree: &Store<Primitive>, prim: Id<Primitive>) -> Option<Aabb<i32>> {
fn or_zip_with<T, F: FnOnce(T, T) -> T>(a: Option<T>, b: Option<T>, f: F) -> Option<T> {
match (a, b) {
(Some(a), Some(b)) => Some(f(a, b)),
(Some(a), _) => Some(a),
(_, b) => b,
}
}
Some(match &tree[prim] {
Primitive::Empty => return None,
Primitive::Aabb(aabb) => *aabb,
Primitive::Pyramid { aabb, .. } => *aabb,
Primitive::And(a, b) => or_zip_with(
self.get_bounds_inner(tree, *a),
self.get_bounds_inner(tree, *b),
|a, b| a.intersection(b),
)?,
Primitive::Or(a, b) | Primitive::Xor(a, b) => or_zip_with(
self.get_bounds_inner(tree, *a),
self.get_bounds_inner(tree, *b),
|a, b| a.union(b),
)?,
})
}
pub fn get_bounds(&self, tree: &Store<Primitive>, prim: Id<Primitive>) -> Aabb<i32> {
self.get_bounds_inner(tree, prim)
.unwrap_or_else(|| Aabb::new_empty(Vec3::zero()))
}
}
pub trait Structure {
fn render<F: FnMut(Primitive) -> Id<Primitive>, G: FnMut(Id<Primitive>, Fill)>(
&self,
site: &Site,
prim: F,
fill: G,
);
// Generate a primitive tree and fills for this structure
fn render_collect(&self, site: &Site) -> (Store<Primitive>, Vec<(Id<Primitive>, Fill)>) {
let mut tree = Store::default();
let mut fills = Vec::new();
self.render(site, |p| tree.insert(p), |p, f| fills.push((p, f)));
(tree, fills)
}
}

708
world/src/site2/mod.rs Normal file
View File

@ -0,0 +1,708 @@
mod gen;
mod plot;
mod tile;
use self::{
gen::{Fill, Primitive, Structure},
plot::{Plot, PlotKind},
tile::{HazardKind, Ori, Tile, TileGrid, TileKind, TILE_SIZE},
};
use crate::{
site::SpawnRules,
util::{attempt, DHashSet, Grid, CARDINALS, SQUARE_4, SQUARE_9},
Canvas, Land,
};
use common::{
astar::Astar,
lottery::Lottery,
spiral::Spiral2d,
store::{Id, Store},
terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
vol::RectVolSize,
};
use hashbrown::hash_map::DefaultHashBuilder;
use rand::prelude::*;
use rand_chacha::ChaChaRng;
use std::ops::Range;
use vek::*;
/// Seed a new RNG from an old RNG, thereby making the old RNG indepedent of
/// changing use of the new RNG. The practical effect of this is to reduce the
/// extent to which changes to child generation algorithm produce a 'butterfly
/// effect' on their parent generators, meaning that generators will be less
/// likely to produce entirely different outcomes if some detail of a generation
/// algorithm changes slightly. This is generally good and makes worldgen code
/// easier to maintain and less liable to breaking changes.
fn reseed(rng: &mut impl Rng) -> impl Rng { ChaChaRng::from_seed(rng.gen::<[u8; 32]>()) }
#[derive(Default)]
pub struct Site {
pub(crate) origin: Vec2<i32>,
tiles: TileGrid,
plots: Store<Plot>,
plazas: Vec<Id<Plot>>,
roads: Vec<Id<Plot>>,
}
impl Site {
pub fn radius(&self) -> f32 {
((self
.tiles
.bounds
.min
.map(|e| e.abs())
.reduce_max()
.max(self.tiles.bounds.max.map(|e| e.abs()).reduce_max())
+ 1)
* tile::TILE_SIZE as i32) as f32
}
pub fn spawn_rules(&self, wpos: Vec2<i32>) -> SpawnRules {
SpawnRules {
trees: SQUARE_9.iter().all(|&rpos| {
self.wpos_tile(wpos + rpos * tile::TILE_SIZE as i32)
.is_empty()
}),
}
}
pub fn bounds(&self) -> Aabr<i32> {
let border = 1;
Aabr {
min: self.origin + self.tile_wpos(self.tiles.bounds.min - border),
max: self.origin + self.tile_wpos(self.tiles.bounds.max + 1 + border),
}
}
pub fn plot(&self, id: Id<Plot>) -> &Plot { &self.plots[id] }
pub fn plots(&self) -> impl Iterator<Item = &Plot> + '_ { self.plots.values() }
pub fn create_plot(&mut self, plot: Plot) -> Id<Plot> { self.plots.insert(plot) }
pub fn blit_aabr(&mut self, aabr: Aabr<i32>, tile: Tile) {
for y in 0..aabr.size().h {
for x in 0..aabr.size().w {
self.tiles.set(aabr.min + Vec2::new(x, y), tile.clone());
}
}
}
pub fn create_road(
&mut self,
land: &Land,
rng: &mut impl Rng,
a: Vec2<i32>,
b: Vec2<i32>,
w: u16,
) -> Option<Id<Plot>> {
const MAX_ITERS: usize = 4096;
let range = -(w as i32) / 2..w as i32 - (w as i32 + 1) / 2;
let heuristic = |tile: &Vec2<i32>| {
let mut max_cost = (tile.distance_squared(b) as f32).sqrt();
for y in range.clone() {
for x in range.clone() {
if self.tiles.get(*tile + Vec2::new(x, y)).is_obstacle() {
max_cost = max_cost.max(1000.0);
} else if !self.tiles.get(*tile + Vec2::new(x, y)).is_empty() {
max_cost = max_cost.max(25.0);
}
}
}
max_cost
};
let path = Astar::new(MAX_ITERS, a, &heuristic, DefaultHashBuilder::default())
.poll(
MAX_ITERS,
&heuristic,
|tile| {
let tile = *tile;
CARDINALS.iter().map(move |dir| tile + *dir)
},
|a, b| {
let alt_a = land.get_alt_approx(self.tile_center_wpos(*a));
let alt_b = land.get_alt_approx(self.tile_center_wpos(*b));
(alt_a - alt_b).abs() / TILE_SIZE as f32
},
|tile| *tile == b,
)
.into_path()?;
let plot = self.create_plot(Plot {
kind: PlotKind::Road(path.clone()),
root_tile: a,
tiles: path.clone().into_iter().collect(),
seed: rng.gen(),
});
self.roads.push(plot);
for (i, &tile) in path.iter().enumerate() {
for y in range.clone() {
for x in range.clone() {
let tile = tile + Vec2::new(x, y);
if self.tiles.get(tile).is_empty() {
self.tiles.set(tile, Tile {
kind: TileKind::Road {
a: i.saturating_sub(1) as u16,
b: (i + 1).min(path.len() - 1) as u16,
w,
},
plot: Some(plot),
});
}
}
}
}
Some(plot)
}
pub fn find_aabr(
&mut self,
search_pos: Vec2<i32>,
area_range: Range<u32>,
min_dims: Extent2<u32>,
) -> Option<(Aabr<i32>, Vec2<i32>)> {
self.tiles.find_near(search_pos, |center, _| {
self.tiles
.grow_aabr(center, area_range.clone(), min_dims)
.ok()
.filter(|aabr| {
(aabr.min.x..aabr.max.x)
.any(|x| self.tiles.get(Vec2::new(x, aabr.min.y - 1)).is_road())
|| (aabr.min.x..aabr.max.x)
.any(|x| self.tiles.get(Vec2::new(x, aabr.max.y)).is_road())
|| (aabr.min.y..aabr.max.y)
.any(|y| self.tiles.get(Vec2::new(aabr.min.x - 1, y)).is_road())
|| (aabr.min.y..aabr.max.y)
.any(|y| self.tiles.get(Vec2::new(aabr.max.x, y)).is_road())
})
})
}
pub fn find_roadside_aabr(
&mut self,
rng: &mut impl Rng,
area_range: Range<u32>,
min_dims: Extent2<u32>,
) -> Option<(Aabr<i32>, Vec2<i32>)> {
let dir = Vec2::<f32>::zero()
.map(|_| rng.gen_range(-1.0..1.0))
.normalized();
let search_pos = if rng.gen() {
self.plot(*self.plazas.choose(rng)?).root_tile
+ (dir * 4.0).map(|e: f32| e.round() as i32)
} else if let PlotKind::Road(path) = &self.plot(*self.roads.choose(rng)?).kind {
*path.nodes().choose(rng)? + (dir * 1.0).map(|e: f32| e.round() as i32)
} else {
unreachable!()
};
self.find_aabr(search_pos, area_range, min_dims)
}
pub fn make_plaza(&mut self, land: &Land, rng: &mut impl Rng) -> Id<Plot> {
let pos = attempt(32, || {
self.plazas
.choose(rng)
.map(|&p| {
self.plot(p).root_tile
+ (Vec2::new(rng.gen_range(-1.0..1.0), rng.gen_range(-1.0..1.0))
.normalized()
* 24.0)
.map(|e| e as i32)
})
.filter(|tile| !self.tiles.get(*tile).is_obstacle())
.filter(|&tile| {
self.plazas
.iter()
.all(|&p| self.plot(p).root_tile.distance_squared(tile) > 20i32.pow(2))
&& rng.gen_range(0..48) > tile.map(|e| e.abs()).reduce_max()
})
})
.unwrap_or_else(Vec2::zero);
let aabr = Aabr {
min: pos + Vec2::broadcast(-3),
max: pos + Vec2::broadcast(4),
};
let plaza = self.create_plot(Plot {
kind: PlotKind::Plaza,
root_tile: pos,
tiles: aabr_tiles(aabr).collect(),
seed: rng.gen(),
});
self.plazas.push(plaza);
self.blit_aabr(aabr, Tile {
kind: TileKind::Plaza,
plot: Some(plaza),
});
let mut already_pathed = vec![plaza];
// One major, one minor road
for width in (1..=2).rev() {
if let Some(&p) = self
.plazas
.iter()
.filter(|p| !already_pathed.contains(p))
.min_by_key(|&&p| self.plot(p).root_tile.distance_squared(pos))
{
self.create_road(land, rng, self.plot(p).root_tile, pos, width);
already_pathed.push(p);
} else {
break;
}
}
plaza
}
pub fn demarcate_obstacles(&mut self, land: &Land) {
const SEARCH_RADIUS: u32 = 96;
Spiral2d::new()
.take((SEARCH_RADIUS * 2 + 1).pow(2) as usize)
.for_each(|tile| {
if let Some(kind) = wpos_is_hazard(land, self.tile_wpos(tile)) {
for &rpos in &SQUARE_4 {
// `get_mut` doesn't increase generation bounds
self.tiles
.get_mut(tile - rpos - 1)
.map(|tile| tile.kind = TileKind::Hazard(kind));
}
}
});
}
pub fn generate(land: &Land, rng: &mut impl Rng, origin: Vec2<i32>) -> Self {
let mut rng = reseed(rng);
let mut site = Site {
origin,
..Site::default()
};
site.demarcate_obstacles(land);
site.make_plaza(land, &mut rng);
let build_chance = Lottery::from(vec![(64.0, 1), (5.0, 2), (8.0, 3), (0.75, 4)]);
let mut castles = 0;
for _ in 0..120 {
match *build_chance.choose_seeded(rng.gen()) {
// House
1 => {
let size = (2.0 + rng.gen::<f32>().powf(8.0) * 3.0).round() as u32;
if let Some((aabr, door_tile)) = attempt(32, || {
site.find_roadside_aabr(
&mut rng,
4..(size + 1).pow(2),
Extent2::broadcast(size),
)
}) {
let plot = site.create_plot(Plot {
kind: PlotKind::House(plot::House::generate(
land,
&mut reseed(&mut rng),
&site,
door_tile,
aabr,
)),
root_tile: aabr.center(),
tiles: aabr_tiles(aabr).collect(),
seed: rng.gen(),
});
site.blit_aabr(aabr, Tile {
kind: TileKind::Building,
plot: Some(plot),
});
} else {
site.make_plaza(land, &mut rng);
}
},
// Guard tower
2 => {
if let Some((aabr, entrance_tile)) = attempt(10, || {
site.find_roadside_aabr(&mut rng, 4..4, Extent2::new(2, 2))
}) {
let plot = site.create_plot(Plot {
kind: PlotKind::Castle(plot::Castle::generate(
land,
&mut rng,
&site,
entrance_tile,
aabr,
)),
root_tile: aabr.center(),
tiles: aabr_tiles(aabr).collect(),
seed: rng.gen(),
});
site.blit_aabr(aabr, Tile {
kind: TileKind::Castle,
plot: Some(plot),
});
}
},
// Field
3 => {
attempt(10, || {
let search_pos = attempt(16, || {
let tile =
(Vec2::new(rng.gen_range(-1.0..1.0), rng.gen_range(-1.0..1.0))
.normalized()
* rng.gen_range(16.0..48.0))
.map(|e| e as i32);
Some(tile).filter(|_| {
site.plazas.iter().all(|&p| {
site.plot(p).root_tile.distance_squared(tile) > 20i32.pow(2)
}) && rng.gen_range(0..48) > tile.map(|e| e.abs()).reduce_max()
})
})
.unwrap_or_else(Vec2::zero);
site.tiles.find_near(search_pos, |center, _| {
site.tiles.grow_organic(&mut rng, center, 12..64).ok()
})
})
.map(|(tiles, _)| {
for tile in tiles {
site.tiles.set(tile, Tile {
kind: TileKind::Field,
plot: None,
});
}
});
},
// Castle
4 if castles < 1 => {
if let Some((aabr, entrance_tile)) = attempt(10, || {
site.find_roadside_aabr(&mut rng, 16 * 16..18 * 18, Extent2::new(16, 16))
}) {
let plot = site.create_plot(Plot {
kind: PlotKind::Castle(plot::Castle::generate(
land,
&mut rng,
&site,
entrance_tile,
aabr,
)),
root_tile: aabr.center(),
tiles: aabr_tiles(aabr).collect(),
seed: rng.gen(),
});
// Walls
site.blit_aabr(aabr, Tile {
kind: TileKind::Wall(Ori::North),
plot: Some(plot),
});
let tower = Tile {
kind: TileKind::Tower,
plot: Some(plot),
};
site.tiles
.set(Vec2::new(aabr.min.x, aabr.min.y), tower.clone());
site.tiles
.set(Vec2::new(aabr.max.x - 1, aabr.min.y), tower.clone());
site.tiles
.set(Vec2::new(aabr.min.x, aabr.max.y - 1), tower.clone());
site.tiles
.set(Vec2::new(aabr.max.x - 1, aabr.max.y - 1), tower.clone());
// Courtyard
site.blit_aabr(
Aabr {
min: aabr.min + 1,
max: aabr.max - 1,
},
Tile {
kind: TileKind::Road { a: 0, b: 0, w: 0 },
plot: Some(plot),
},
);
// Keep
site.blit_aabr(
Aabr {
min: aabr.center() - 3,
max: aabr.center() + 3,
},
Tile {
kind: TileKind::Wall(Ori::North),
plot: Some(plot),
},
);
site.tiles.set(
Vec2::new(aabr.center().x + 2, aabr.center().y + 2),
tower.clone(),
);
site.tiles.set(
Vec2::new(aabr.center().x + 2, aabr.center().y - 3),
tower.clone(),
);
site.tiles.set(
Vec2::new(aabr.center().x - 3, aabr.center().y + 2),
tower.clone(),
);
site.tiles.set(
Vec2::new(aabr.center().x - 3, aabr.center().y - 3),
tower.clone(),
);
site.blit_aabr(
Aabr {
min: aabr.center() - 2,
max: aabr.center() + 2,
},
Tile {
kind: TileKind::Keep(tile::KeepKind::Middle),
plot: Some(plot),
},
);
castles += 1;
}
},
_ => {},
}
}
site
}
pub fn wpos_tile_pos(&self, wpos2d: Vec2<i32>) -> Vec2<i32> {
(wpos2d - self.origin).map(|e| e.div_euclid(TILE_SIZE as i32))
}
pub fn wpos_tile(&self, wpos2d: Vec2<i32>) -> &Tile {
self.tiles.get(self.wpos_tile_pos(wpos2d))
}
pub fn tile_wpos(&self, tile: Vec2<i32>) -> Vec2<i32> {
self.origin + tile * tile::TILE_SIZE as i32
}
pub fn tile_center_wpos(&self, tile: Vec2<i32>) -> Vec2<i32> {
self.origin + tile * tile::TILE_SIZE as i32 + tile::TILE_SIZE as i32 / 2
}
pub fn render_tile(&self, canvas: &mut Canvas, _dynamic_rng: &mut impl Rng, tpos: Vec2<i32>) {
let tile = self.tiles.get(tpos);
let twpos = self.tile_wpos(tpos);
let border = TILE_SIZE as i32;
let cols = (-border..TILE_SIZE as i32 + border)
.map(|y| {
(-border..TILE_SIZE as i32 + border)
.map(move |x| (twpos + Vec2::new(x, y), Vec2::new(x, y)))
})
.flatten();
#[allow(clippy::single_match)]
match &tile.kind {
TileKind::Plaza => {
let near_roads = CARDINALS.iter().filter_map(|rpos| {
if self.tiles.get(tpos + rpos) == tile {
Some(Aabr {
min: self.tile_wpos(tpos).map(|e| e as f32),
max: self.tile_wpos(tpos + 1).map(|e| e as f32),
})
} else {
None
}
});
cols.for_each(|(wpos2d, _offs)| {
let wpos2df = wpos2d.map(|e| e as f32);
let dist = near_roads
.clone()
.map(|aabr| aabr.distance_to_point(wpos2df))
.min_by_key(|d| (*d * 100.0) as i32);
if dist.map_or(false, |d| d <= 1.5) {
let alt = canvas.col(wpos2d).map_or(0, |col| col.alt as i32);
(-8..6).for_each(|z| {
canvas.map(Vec3::new(wpos2d.x, wpos2d.y, alt + z), |b| {
if z >= 0 {
if b.is_filled() {
Block::empty()
} else {
b.with_sprite(SpriteKind::Empty)
}
} else {
Block::new(BlockKind::Rock, Rgb::new(55, 45, 50))
}
})
});
}
});
},
_ => {},
}
}
pub fn render(&self, canvas: &mut Canvas, dynamic_rng: &mut impl Rng) {
canvas.foreach_col(|canvas, wpos2d, col| {
let tpos = self.wpos_tile_pos(wpos2d);
let near_roads = SQUARE_9
.iter()
.filter_map(|rpos| {
let tile = self.tiles.get(tpos + rpos);
if let TileKind::Road { a, b, w } = &tile.kind {
if let Some(PlotKind::Road(path)) = tile.plot.map(|p| &self.plot(p).kind) {
Some((LineSegment2 {
start: self.tile_center_wpos(path.nodes()[*a as usize]).map(|e| e as f32),
end: self.tile_center_wpos(path.nodes()[*b as usize]).map(|e| e as f32),
}, *w))
} else {
None
}
} else {
None
}
});
let wpos2df = wpos2d.map(|e| e as f32);
let dist = near_roads
.map(|(line, w)| (line.distance_to_point(wpos2df) - w as f32 * 2.0).max(0.0))
.min_by_key(|d| (*d * 100.0) as i32);
if dist.map_or(false, |d| d <= 0.75) {
let alt = canvas.col(wpos2d).map_or(0, |col| col.alt as i32);
(-6..4).for_each(|z| canvas.map(
Vec3::new(wpos2d.x, wpos2d.y, alt + z),
|b| if z >= 0 {
if b.is_filled() {
Block::empty()
} else {
b.with_sprite(SpriteKind::Empty)
}
} else {
Block::new(BlockKind::Rock, Rgb::new(55, 45, 50))
},
));
}
let tile = self.wpos_tile(wpos2d);
let seed = tile.plot.map_or(0, |p| self.plot(p).seed);
#[allow(clippy::single_match)]
match tile.kind {
TileKind::Field /*| TileKind::Road*/ => (-4..5).for_each(|z| canvas.map(
Vec3::new(wpos2d.x, wpos2d.y, col.alt as i32 + z),
|b| if [
BlockKind::Grass,
BlockKind::Earth,
BlockKind::Sand,
BlockKind::Snow,
BlockKind::Rock,
]
.contains(&b.kind()) {
match tile.kind {
TileKind::Field => Block::new(BlockKind::Earth, Rgb::new(40, 5 + (seed % 32) as u8, 0)),
TileKind::Road { .. } => Block::new(BlockKind::Rock, Rgb::new(55, 45, 65)),
_ => unreachable!(),
}
} else {
b.with_sprite(SpriteKind::Empty)
},
)),
// TileKind::Building => {
// let base_alt = tile.plot.map(|p| self.plot(p)).map_or(col.alt as i32, |p| p.base_alt);
// for z in base_alt - 12..base_alt + 16 {
// canvas.set(
// Vec3::new(wpos2d.x, wpos2d.y, z),
// Block::new(BlockKind::Wood, Rgb::new(180, 90 + (seed % 64) as u8, 120))
// );
// }
// },
// TileKind::Castle | TileKind::Wall => {
// let base_alt = tile.plot.map(|p| self.plot(p)).map_or(col.alt as i32, |p| p.base_alt);
// for z in base_alt - 12..base_alt + if tile.kind == TileKind::Wall { 24 } else { 40 } {
// canvas.set(
// Vec3::new(wpos2d.x, wpos2d.y, z),
// Block::new(BlockKind::Wood, Rgb::new(40, 40, 55))
// );
// }
// },
_ => {},
}
});
let tile_aabr = Aabr {
min: self.wpos_tile_pos(canvas.wpos()) - 1,
max: self
.wpos_tile_pos(canvas.wpos() + TerrainChunkSize::RECT_SIZE.map(|e| e as i32) + 2)
+ 3, // Round up, uninclusive, border
};
// Don't double-generate the same plot per chunk!
let mut plots = DHashSet::default();
for y in tile_aabr.min.y..tile_aabr.max.y {
for x in tile_aabr.min.x..tile_aabr.max.x {
self.render_tile(canvas, dynamic_rng, Vec2::new(x, y));
if let Some(plot) = self.tiles.get(Vec2::new(x, y)).plot {
plots.insert(plot);
}
}
}
let mut plots_to_render = plots.into_iter().collect::<Vec<_>>();
plots_to_render.sort_unstable();
for plot in plots_to_render {
let (prim_tree, fills) = match &self.plots[plot].kind {
PlotKind::House(house) => house.render_collect(self),
PlotKind::Castle(castle) => castle.render_collect(self),
_ => continue,
};
for (prim, fill) in fills {
let aabb = fill.get_bounds(&prim_tree, prim);
for x in aabb.min.x..aabb.max.x {
for y in aabb.min.y..aabb.max.y {
for z in aabb.min.z..aabb.max.z {
let pos = Vec3::new(x, y, z);
if let Some(block) = fill.sample_at(&prim_tree, prim, pos) {
canvas.set(pos, block);
}
}
}
}
}
}
}
}
pub fn test_site() -> Site { Site::generate(&Land::empty(), &mut thread_rng(), Vec2::zero()) }
fn wpos_is_hazard(land: &Land, wpos: Vec2<i32>) -> Option<HazardKind> {
if land
.get_chunk_at(wpos)
.map_or(true, |c| c.river.near_water())
{
Some(HazardKind::Water)
} else if let Some(gradient) = Some(land.get_gradient_approx(wpos)).filter(|g| *g > 0.8) {
Some(HazardKind::Hill { gradient })
} else {
None
}
}
pub fn aabr_tiles(aabr: Aabr<i32>) -> impl Iterator<Item = Vec2<i32>> {
(0..aabr.size().h)
.map(move |y| (0..aabr.size().w).map(move |x| aabr.min + Vec2::new(x, y)))
.flatten()
}
pub struct Plaza {}

33
world/src/site2/plot.rs Normal file
View File

@ -0,0 +1,33 @@
mod castle;
mod house;
pub use self::{castle::Castle, house::House};
use super::*;
use crate::util::DHashSet;
use common::path::Path;
use vek::*;
pub struct Plot {
pub(crate) kind: PlotKind,
pub(crate) root_tile: Vec2<i32>,
pub(crate) tiles: DHashSet<Vec2<i32>>,
pub(crate) seed: u32,
}
impl Plot {
pub fn find_bounds(&self) -> Aabr<i32> {
self.tiles
.iter()
.fold(Aabr::new_empty(self.root_tile), |b, t| {
b.expanded_to_contain_point(*t)
})
}
}
pub enum PlotKind {
House(House),
Plaza,
Castle(Castle),
Road(Path<Vec2<i32>>),
}

View File

@ -0,0 +1,226 @@
use super::*;
use crate::Land;
use common::terrain::{Block, BlockKind};
use rand::prelude::*;
use vek::*;
pub struct Castle {
_entrance_tile: Vec2<i32>,
tile_aabr: Aabr<i32>,
_bounds: Aabr<i32>,
alt: i32,
}
impl Castle {
pub fn generate(
land: &Land,
_rng: &mut impl Rng,
site: &Site,
entrance_tile: Vec2<i32>,
tile_aabr: Aabr<i32>,
) -> Self {
Self {
_entrance_tile: entrance_tile,
tile_aabr,
_bounds: Aabr {
min: site.tile_wpos(tile_aabr.min),
max: site.tile_wpos(tile_aabr.max),
},
alt: land.get_alt_approx(site.tile_center_wpos(entrance_tile)) as i32,
}
}
}
impl Structure for Castle {
fn render<F: FnMut(Primitive) -> Id<Primitive>, G: FnMut(Id<Primitive>, Fill)>(
&self,
site: &Site,
mut prim: F,
mut fill: G,
) {
let wall_height = 24;
let _thickness = 12;
let parapet_height = 2;
let parapet_width = 1;
let _downwards = 40;
let tower_height = 12;
let keep_levels = 3;
let keep_level_height = 8;
let _keep_height = wall_height + keep_levels * keep_level_height + 1;
for x in 0..self.tile_aabr.size().w {
for y in 0..self.tile_aabr.size().h {
let tile_pos = self.tile_aabr.min + Vec2::new(x, y);
let _wpos_center = site.tile_center_wpos(tile_pos);
let wpos = site.tile_wpos(tile_pos);
let ori = if x == 0 || x == self.tile_aabr.size().w - 1 {
Vec2::new(1, 0)
} else {
Vec2::new(0, 1)
};
let ori_tower_x = if x == 0 {
Vec2::new(1, 0)
} else {
Vec2::new(0, 0)
};
let ori_tower_y = if y == 0 {
Vec2::new(0, 1)
} else {
Vec2::new(0, 0)
};
let ori_tower = ori_tower_x + ori_tower_y;
match site.tiles.get(tile_pos).kind.clone() {
TileKind::Wall(_ori) => {
let wall = prim(Primitive::Aabb(Aabb {
min: wpos.with_z(self.alt),
max: (wpos + 6).with_z(self.alt + wall_height + parapet_height),
}));
let cut_path = prim(Primitive::Aabb(Aabb {
min: (wpos + (parapet_width * ori) as Vec2<i32>)
.with_z(self.alt + wall_height),
max: (wpos
+ (6 - parapet_width) * ori as Vec2<i32>
+ 6 * ori.yx() as Vec2<i32>)
.with_z(self.alt + wall_height + parapet_height),
}));
let cut_sides1 = prim(Primitive::Aabb(Aabb {
min: Vec3::new(wpos.x, wpos.y, self.alt + wall_height + 1),
max: Vec3::new(
wpos.x + 6 * ori.x + ori.y,
wpos.y + 6 * ori.y + ori.x,
self.alt + wall_height + parapet_height,
),
}));
let pillar_start = prim(Primitive::Aabb(Aabb {
min: Vec3::new(wpos.x, wpos.y - 1, self.alt),
max: Vec3::new(wpos.x + 1, wpos.y + 7, self.alt + wall_height),
}));
let pillar_end = prim(Primitive::Aabb(Aabb {
min: Vec3::new(wpos.x + 5, wpos.y - 1, self.alt),
max: Vec3::new(wpos.x + 6, wpos.y + 7, self.alt + wall_height),
}));
let pillars = prim(Primitive::Or(pillar_start, pillar_end));
fill(
prim(Primitive::Or(wall, pillars)),
Fill::Block(Block::new(BlockKind::Rock, Rgb::new(33, 33, 33))),
);
fill(cut_path, Fill::Block(Block::empty()));
fill(cut_sides1, Fill::Block(Block::empty()));
},
TileKind::Tower => {
let tower_lower = prim(Primitive::Aabb(Aabb {
min: wpos.with_z(self.alt),
max: (wpos + 6).with_z(self.alt + wall_height + tower_height),
}));
let tower_lower_inner_x = prim(Primitive::Aabb(Aabb {
min: Vec3::new(
wpos.x + ori_tower.x,
wpos.y + parapet_width,
self.alt + wall_height,
),
max: Vec3::new(
wpos.x + 6 + ori_tower.x - 1,
wpos.y + 6 - parapet_width,
self.alt + wall_height + tower_height / 3,
),
}));
let tower_lower_inner_y = prim(Primitive::Aabb(Aabb {
min: Vec3::new(
wpos.x + parapet_width,
wpos.y + ori_tower.y,
self.alt + wall_height,
),
max: Vec3::new(
wpos.x + 6 - parapet_width,
wpos.y + 6 + ori_tower.y - 1,
self.alt + wall_height + tower_height / 3,
),
}));
let tower_lower_inner =
prim(Primitive::Or(tower_lower_inner_x, tower_lower_inner_y));
fill(
prim(Primitive::Xor(tower_lower, tower_lower_inner)),
Fill::Block(Block::new(BlockKind::Rock, Rgb::new(33, 33, 33))),
);
let tower_upper = prim(Primitive::Aabb(Aabb {
min: Vec3::new(
wpos.x - 1,
wpos.y - 1,
self.alt + wall_height + tower_height - 3i32,
),
max: Vec3::new(
wpos.x + 7,
wpos.y + 7,
self.alt + wall_height + tower_height - 1i32,
),
}));
let tower_upper2 = prim(Primitive::Aabb(Aabb {
min: Vec3::new(
wpos.x - 2,
wpos.y - 2,
self.alt + wall_height + tower_height - 1i32,
),
max: Vec3::new(
wpos.x + 8,
wpos.y + 8,
self.alt + wall_height + tower_height,
),
}));
fill(
prim(Primitive::Or(tower_upper, tower_upper2)),
Fill::Block(Block::new(BlockKind::Rock, Rgb::new(33, 33, 33))),
);
let roof_lip = 1;
let roof_height = 8 / 2 + roof_lip + 1;
// Roof
fill(
prim(Primitive::Pyramid {
aabb: Aabb {
min: (wpos - 2 - roof_lip)
.with_z(self.alt + wall_height + tower_height),
max: (wpos + 8 + roof_lip).with_z(
self.alt + wall_height + tower_height + roof_height,
),
},
inset: roof_height,
}),
Fill::Block(Block::new(BlockKind::Wood, Rgb::new(116, 20, 20))),
);
},
TileKind::Keep(kind) => {
match kind {
tile::KeepKind::Middle => {
for i in 0..keep_levels + 1 {
let height = keep_level_height * i;
fill(
prim(Primitive::Aabb(Aabb {
min: wpos.with_z(self.alt + height),
max: (wpos + 6).with_z(self.alt + height + 1),
})),
Fill::Block(Block::new(
BlockKind::Rock,
Rgb::new(89, 44, 14),
)),
);
}
},
tile::KeepKind::Corner => {},
tile::KeepKind::Wall(_ori) => {
for i in 0..keep_levels + 1 {
let _height = keep_level_height * i;
// TODO clamp value in case of big heights
let _window_height = keep_level_height - 3;
}
},
}
},
_ => {},
}
}
}
}
}

View File

@ -0,0 +1,188 @@
use super::*;
use crate::Land;
use common::terrain::{Block, BlockKind, SpriteKind};
use rand::prelude::*;
use vek::*;
pub struct House {
_door_tile: Vec2<i32>,
tile_aabr: Aabr<i32>,
bounds: Aabr<i32>,
alt: i32,
levels: u32,
roof_color: Rgb<u8>,
}
impl House {
pub fn generate(
land: &Land,
rng: &mut impl Rng,
site: &Site,
door_tile: Vec2<i32>,
tile_aabr: Aabr<i32>,
) -> Self {
Self {
_door_tile: door_tile,
tile_aabr,
bounds: Aabr {
min: site.tile_wpos(tile_aabr.min),
max: site.tile_wpos(tile_aabr.max),
},
alt: land.get_alt_approx(site.tile_center_wpos(door_tile)) as i32 + 2,
levels: rng.gen_range(1..2 + (tile_aabr.max - tile_aabr.min).product() / 6) as u32,
roof_color: {
let colors = [
Rgb::new(21, 43, 48),
Rgb::new(11, 23, 38),
Rgb::new(45, 28, 21),
Rgb::new(10, 55, 40),
Rgb::new(5, 35, 15),
Rgb::new(40, 5, 11),
Rgb::new(55, 45, 11),
];
*colors.choose(rng).unwrap()
},
}
}
}
impl Structure for House {
fn render<F: FnMut(Primitive) -> Id<Primitive>, G: FnMut(Id<Primitive>, Fill)>(
&self,
site: &Site,
mut prim: F,
mut fill: G,
) {
let storey = 5;
let roof = storey * self.levels as i32;
let foundations = 12;
// Walls
let inner = prim(Primitive::Aabb(Aabb {
min: (self.bounds.min + 1).with_z(self.alt),
max: self.bounds.max.with_z(self.alt + roof),
}));
let outer = prim(Primitive::Aabb(Aabb {
min: self.bounds.min.with_z(self.alt - foundations),
max: (self.bounds.max + 1).with_z(self.alt + roof),
}));
fill(
outer,
Fill::Brick(BlockKind::Rock, Rgb::new(80, 75, 85), 24),
);
fill(inner, Fill::Block(Block::empty()));
let walls = prim(Primitive::Xor(outer, inner));
// wall pillars
let mut pillars_y = prim(Primitive::Empty);
for x in self.tile_aabr.min.x..self.tile_aabr.max.x + 2 {
let pillar = prim(Primitive::Aabb(Aabb {
min: site
.tile_wpos(Vec2::new(x, self.tile_aabr.min.y))
.with_z(self.alt),
max: (site.tile_wpos(Vec2::new(x, self.tile_aabr.max.y + 1)) + Vec2::unit_x())
.with_z(self.alt + roof),
}));
pillars_y = prim(Primitive::Or(pillars_y, pillar));
}
let mut pillars_x = prim(Primitive::Empty);
for y in self.tile_aabr.min.y..self.tile_aabr.max.y + 2 {
let pillar = prim(Primitive::Aabb(Aabb {
min: site
.tile_wpos(Vec2::new(self.tile_aabr.min.x, y))
.with_z(self.alt),
max: (site.tile_wpos(Vec2::new(self.tile_aabr.max.x + 1, y)) + Vec2::unit_y())
.with_z(self.alt + roof),
}));
pillars_x = prim(Primitive::Or(pillars_x, pillar));
}
let pillars = prim(Primitive::And(pillars_x, pillars_y));
fill(
pillars,
Fill::Block(Block::new(BlockKind::Wood, Rgb::new(55, 25, 8))),
);
// For each storey...
for i in 0..self.levels + 1 {
let height = storey * i as i32;
let window_height = storey - 3;
// Windows x axis
{
let mut windows = prim(Primitive::Empty);
for y in self.tile_aabr.min.y..self.tile_aabr.max.y {
let window = prim(Primitive::Aabb(Aabb {
min: (site.tile_wpos(Vec2::new(self.tile_aabr.min.x, y))
+ Vec2::unit_y() * 2)
.with_z(self.alt + height + 2),
max: (site.tile_wpos(Vec2::new(self.tile_aabr.max.x, y + 1))
+ Vec2::new(1, -1))
.with_z(self.alt + height + 2 + window_height),
}));
windows = prim(Primitive::Or(windows, window));
}
fill(
prim(Primitive::And(walls, windows)),
Fill::Block(Block::air(SpriteKind::Window1).with_ori(2).unwrap()),
);
}
// Windows y axis
{
let mut windows = prim(Primitive::Empty);
for x in self.tile_aabr.min.x..self.tile_aabr.max.x {
let window = prim(Primitive::Aabb(Aabb {
min: (site.tile_wpos(Vec2::new(x, self.tile_aabr.min.y))
+ Vec2::unit_x() * 2)
.with_z(self.alt + height + 2),
max: (site.tile_wpos(Vec2::new(x + 1, self.tile_aabr.max.y))
+ Vec2::new(-1, 1))
.with_z(self.alt + height + 2 + window_height),
}));
windows = prim(Primitive::Or(windows, window));
}
fill(
prim(Primitive::And(walls, windows)),
Fill::Block(Block::air(SpriteKind::Window1).with_ori(0).unwrap()),
);
}
// Floor
fill(
prim(Primitive::Aabb(Aabb {
min: (self.bounds.min + 1).with_z(self.alt + height),
max: self.bounds.max.with_z(self.alt + height + 1),
})),
Fill::Block(Block::new(BlockKind::Rock, Rgb::new(89, 44, 14))),
);
}
let roof_lip = 2;
let roof_height = (self.bounds.min - self.bounds.max)
.map(|e| e.abs())
.reduce_min()
/ 2
+ roof_lip
+ 1;
// Roof
fill(
prim(Primitive::Pyramid {
aabb: Aabb {
min: (self.bounds.min - roof_lip).with_z(self.alt + roof),
max: (self.bounds.max + 1 + roof_lip).with_z(self.alt + roof + roof_height),
},
inset: roof_height,
}),
Fill::Block(Block::new(BlockKind::Wood, self.roof_color)),
);
// Foundations
fill(
prim(Primitive::Aabb(Aabb {
min: (self.bounds.min - 1).with_z(self.alt - foundations),
max: (self.bounds.max + 2).with_z(self.alt + 1),
})),
Fill::Block(Block::new(BlockKind::Rock, Rgb::new(31, 33, 32))),
);
}
}

243
world/src/site2/tile.rs Normal file
View File

@ -0,0 +1,243 @@
use super::*;
use crate::util::DHashSet;
use common::spiral::Spiral2d;
use std::ops::Range;
pub const TILE_SIZE: u32 = 6;
pub const ZONE_SIZE: u32 = 16;
pub const ZONE_RADIUS: u32 = 16;
pub const TILE_RADIUS: u32 = ZONE_SIZE * ZONE_RADIUS;
#[allow(dead_code)]
pub const MAX_BLOCK_RADIUS: u32 = TILE_SIZE * TILE_RADIUS;
pub struct TileGrid {
pub(crate) bounds: Aabr<i32>, // Inclusive
zones: Grid<Option<Grid<Option<Tile>>>>,
}
impl Default for TileGrid {
fn default() -> Self {
Self {
bounds: Aabr::new_empty(Vec2::zero()),
zones: Grid::populate_from(Vec2::broadcast(ZONE_RADIUS as i32 * 2 + 1), |_| None),
}
}
}
impl TileGrid {
pub fn get(&self, tpos: Vec2<i32>) -> &Tile {
static EMPTY: Tile = Tile::empty();
let tpos = tpos + TILE_RADIUS as i32;
self.zones
.get(tpos.map(|e| e.div_euclid(ZONE_SIZE as i32)))
.and_then(|zone| {
zone.as_ref()?
.get(tpos.map(|e| e.rem_euclid(ZONE_SIZE as i32)))
})
.and_then(|tile| tile.as_ref())
.unwrap_or(&EMPTY)
}
// WILL NOT EXPAND BOUNDS!
pub fn get_mut(&mut self, tpos: Vec2<i32>) -> Option<&mut Tile> {
let tpos = tpos + TILE_RADIUS as i32;
self.zones
.get_mut(tpos.map(|e| e.div_euclid(ZONE_SIZE as i32)))
.and_then(|zone| {
zone.get_or_insert_with(|| {
Grid::populate_from(Vec2::broadcast(ZONE_SIZE as i32), |_| None)
})
.get_mut(tpos.map(|e| e.rem_euclid(ZONE_SIZE as i32)))
.map(|tile| tile.get_or_insert_with(Tile::empty))
})
}
pub fn set(&mut self, tpos: Vec2<i32>, tile: Tile) -> Option<Tile> {
self.bounds.expand_to_contain_point(tpos);
self.get_mut(tpos).map(|t| std::mem::replace(t, tile))
}
pub fn find_near<R>(
&self,
tpos: Vec2<i32>,
mut f: impl FnMut(Vec2<i32>, &Tile) -> Option<R>,
) -> Option<(R, Vec2<i32>)> {
const MAX_SEARCH_RADIUS_BLOCKS: u32 = 70;
const MAX_SEARCH_CELLS: u32 = ((MAX_SEARCH_RADIUS_BLOCKS / TILE_SIZE) * 2 + 1).pow(2);
Spiral2d::new()
.take(MAX_SEARCH_CELLS as usize)
.map(|r| tpos + r)
.find_map(|tpos| (&mut f)(tpos, self.get(tpos)).zip(Some(tpos)))
}
pub fn grow_aabr(
&self,
center: Vec2<i32>,
area_range: Range<u32>,
min_dims: Extent2<u32>,
) -> Result<Aabr<i32>, Aabr<i32>> {
let mut aabr = Aabr {
min: center,
max: center + 1,
};
if !self.get(center).is_empty() {
return Err(aabr);
};
let mut last_growth = 0;
for i in 0..32 {
if i - last_growth >= 4
|| aabr.size().product()
+ if i % 2 == 0 {
aabr.size().h
} else {
aabr.size().w
}
> area_range.end as i32
{
break;
} else {
// `center.sum()` to avoid biasing certain directions
match (i + center.sum().abs()) % 4 {
0 if (aabr.min.y..aabr.max.y + 1)
.all(|y| self.get(Vec2::new(aabr.max.x, y)).is_empty()) =>
{
aabr.max.x += 1;
last_growth = i;
}
1 if (aabr.min.x..aabr.max.x + 1)
.all(|x| self.get(Vec2::new(x, aabr.max.y)).is_empty()) =>
{
aabr.max.y += 1;
last_growth = i;
}
2 if (aabr.min.y..aabr.max.y + 1)
.all(|y| self.get(Vec2::new(aabr.min.x - 1, y)).is_empty()) =>
{
aabr.min.x -= 1;
last_growth = i;
}
3 if (aabr.min.x..aabr.max.x + 1)
.all(|x| self.get(Vec2::new(x, aabr.min.y - 1)).is_empty()) =>
{
aabr.min.y -= 1;
last_growth = i;
}
_ => {},
}
}
}
if aabr.size().product() as u32 >= area_range.start
&& aabr.size().w as u32 >= min_dims.w
&& aabr.size().h as u32 >= min_dims.h
{
Ok(aabr)
} else {
Err(aabr)
}
}
pub fn grow_organic(
&self,
rng: &mut impl Rng,
center: Vec2<i32>,
area_range: Range<u32>,
) -> Result<DHashSet<Vec2<i32>>, DHashSet<Vec2<i32>>> {
let mut tiles = DHashSet::default();
let mut open = Vec::new();
tiles.insert(center);
open.push(center);
while tiles.len() < area_range.end as usize && !open.is_empty() {
let tile = open.remove(rng.gen_range(0..open.len()));
for &rpos in CARDINALS.iter() {
let neighbor = tile + rpos;
if self.get(neighbor).is_empty() && !tiles.contains(&neighbor) {
tiles.insert(neighbor);
open.push(neighbor);
}
}
}
if tiles.len() >= area_range.start as usize {
Ok(tiles)
} else {
Err(tiles)
}
}
}
#[derive(Clone, PartialEq)]
pub enum TileKind {
Empty,
Hazard(HazardKind),
Field,
Plaza,
Road { a: u16, b: u16, w: u16 },
Building,
Castle,
Wall(Ori),
Tower,
Keep(KeepKind),
}
#[derive(Clone, PartialEq)]
pub struct Tile {
pub(crate) kind: TileKind,
pub(crate) plot: Option<Id<Plot>>,
}
impl Tile {
pub const fn empty() -> Self {
Self {
kind: TileKind::Empty,
plot: None,
}
}
/// Create a tile that is not associated with any plot.
pub const fn free(kind: TileKind) -> Self { Self { kind, plot: None } }
pub fn is_empty(&self) -> bool { self.kind == TileKind::Empty }
pub fn is_road(&self) -> bool { matches!(self.kind, TileKind::Road { .. }) }
pub fn is_obstacle(&self) -> bool {
matches!(
self.kind,
TileKind::Hazard(_) | TileKind::Building | TileKind::Castle | TileKind::Wall(_)
)
}
}
#[derive(Copy, Clone, PartialEq)]
pub enum HazardKind {
Water,
Hill { gradient: f32 },
}
#[derive(Copy, Clone, PartialEq)]
pub enum KeepKind {
Middle,
Corner,
Wall(Ori),
}
#[repr(u8)]
#[derive(Copy, Clone, PartialEq)]
pub enum Ori {
North = 0,
East = 1,
South = 2,
West = 3,
}
impl Ori {
pub fn dir(self) -> Vec2<i32> { CARDINALS[self as u8 as usize] }
}

View File

@ -24,6 +24,30 @@ impl Sampler<'static> for FastNoise {
type Sample = f32;
fn get(&self, pos: Self::Index) -> Self::Sample {
// let align_pos = pos.map(|e| e.floor());
// let near_pos = align_pos.map(|e| e as i32);
// let v000 = self.noise_at(near_pos + Vec3::new(0, 0, 0));
// let v100 = self.noise_at(near_pos + Vec3::new(1, 0, 0));
// let v010 = self.noise_at(near_pos + Vec3::new(0, 1, 0));
// let v110 = self.noise_at(near_pos + Vec3::new(1, 1, 0));
// let v001 = self.noise_at(near_pos + Vec3::new(0, 0, 1));
// let v101 = self.noise_at(near_pos + Vec3::new(1, 0, 1));
// let v011 = self.noise_at(near_pos + Vec3::new(0, 1, 1));
// let v111 = self.noise_at(near_pos + Vec3::new(1, 1, 1));
// let factor = (pos - align_pos).map(|e| e as f32);
// let v00 = v000 + factor.z * (v001 - v000);
// let v10 = v010 + factor.z * (v011 - v010);
// let v01 = v100 + factor.z * (v101 - v100);
// let v11 = v110 + factor.z * (v111 - v110);
// let v0 = v00 + factor.y * (v01 - v00);
// let v1 = v10 + factor.y * (v11 - v10);
// (v0 + factor.x * (v1 - v0)) * 2.0 - 1.0
let near_pos = pos.map(|e| e.floor() as i32);
let v000 = self.noise_at(near_pos + Vec3::new(0, 0, 0));
@ -51,3 +75,44 @@ impl Sampler<'static> for FastNoise {
(y0 + factor.z * (y1 - y0)) * 2.0 - 1.0
}
}
pub struct FastNoise2d {
noise: RandomField,
}
impl FastNoise2d {
pub const fn new(seed: u32) -> Self {
Self {
noise: RandomField::new(seed),
}
}
#[allow(clippy::excessive_precision)] // TODO: Pending review in #587
fn noise_at(&self, pos: Vec2<i32>) -> f32 {
(self.noise.get(Vec3::new(pos.x, pos.y, 0)) & 4095) as f32 * 0.000244140625
}
}
impl Sampler<'static> for FastNoise2d {
type Index = Vec2<f64>;
type Sample = f32;
fn get(&self, pos: Self::Index) -> Self::Sample {
let near_pos = pos.map(|e| e.floor() as i32);
let v00 = self.noise_at(near_pos + Vec2::new(0, 0));
let v10 = self.noise_at(near_pos + Vec2::new(1, 0));
let v01 = self.noise_at(near_pos + Vec2::new(0, 1));
let v11 = self.noise_at(near_pos + Vec2::new(1, 1));
let factor = pos.map(|e| {
let f = e.fract().add(1.0).fract() as f32;
f.powi(2) * (3.0 - 2.0 * f)
});
let v0 = v00 + factor.y * (v10 - v00);
let v1 = v01 + factor.y * (v11 - v01);
(v0 + factor.x * (v1 - v0)) * 2.0 - 1.0
}
}

12
world/src/util/math.rs Normal file
View File

@ -0,0 +1,12 @@
use std::ops::Range;
use vek::*;
/// Return a value between 0 and 1 corresponding to how close to the centre of
/// `range` `x` is. The exact function used is left unspecified, but it shall
/// have the shape of a bell-like curve. This function is required to return `0`
/// (or a value extremely close to `0`) when `x` is outside of `range`.
pub fn close(x: f32, range: Range<f32>) -> f32 {
let mean = (range.start + range.end) / 2.0;
let width = (range.end - range.start) / 2.0;
(1.0 - ((x - mean) / width).clamped(-1.0, 1.0).powi(2)).powi(2)
}

View File

@ -1,5 +1,6 @@
pub mod fast_noise;
pub mod map_vec;
pub mod math;
pub mod random;
pub mod sampler;
pub mod seed_expan;
@ -9,7 +10,7 @@ pub mod unit_chooser;
// Reexports
pub use self::{
fast_noise::FastNoise,
fast_noise::{FastNoise, FastNoise2d},
map_vec::MapVec,
random::{RandomField, RandomPerm},
sampler::{Sampler, SamplerMut},
@ -81,3 +82,22 @@ pub const CARDINAL_LOCALITY: [Vec2<i32>; 5] = [
Vec2::new(0, -1),
Vec2::new(-1, 0),
];
pub const SQUARE_4: [Vec2<i32>; 4] = [
Vec2::new(0, 0),
Vec2::new(1, 0),
Vec2::new(0, 1),
Vec2::new(1, 1),
];
pub const SQUARE_9: [Vec2<i32>; 9] = [
Vec2::new(-1, -1),
Vec2::new(0, -1),
Vec2::new(1, -1),
Vec2::new(-1, 0),
Vec2::new(0, 0),
Vec2::new(1, 0),
Vec2::new(-1, 1),
Vec2::new(0, 1),
Vec2::new(1, 1),
];

View File

@ -64,7 +64,7 @@ impl Sampler<'static> for RandomPerm {
// `RandomPerm` is not high-quality but it is at least fast and deterministic.
impl RngCore for RandomPerm {
fn next_u32(&mut self) -> u32 {
self.seed = self.get(self.seed);
self.seed = self.get(self.seed) ^ 0xA7535839;
self.seed
}