diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e221fe66..4976a302f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index e9dcba0ffc..5cff08740c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/assets/voxygen/element/map/excl.png b/assets/voxygen/element/map/excl.png new file mode 100644 index 0000000000..15770458c3 --- /dev/null +++ b/assets/voxygen/element/map/excl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6450c2f8c4a97c5ca4ffe3bedc2cf9ed0b36fd60c146d8f8b3c4d326d9b771ec +size 10816 diff --git a/assets/voxygen/element/map/tree.png b/assets/voxygen/element/map/tree.png new file mode 100644 index 0000000000..791e7f167a --- /dev/null +++ b/assets/voxygen/element/map/tree.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecec258c4376a9aa48ef40dd3e9b32cdbb233677c30081f0ee5bd1407f29307a +size 231 diff --git a/assets/voxygen/element/map/tree_hover.png b/assets/voxygen/element/map/tree_hover.png new file mode 100644 index 0000000000..6cee8c17ae --- /dev/null +++ b/assets/voxygen/element/map/tree_hover.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:441e164e75d4f30ab06bb59e9cae937a81bb085cff4d622f5056e88291dcdf6f +size 274 diff --git a/assets/voxygen/i18n/en/hud/map.ron b/assets/voxygen/i18n/en/hud/map.ron index ee0db0fe71..8482ecfbc2 100644 --- a/assets/voxygen/i18n/en/hud/map.ron +++ b/assets/voxygen/i18n/en/hud/map.ron @@ -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", diff --git a/assets/voxygen/shaders/figure-frag.glsl b/assets/voxygen/shaders/figure-frag.glsl index 3b69a17d8b..53efa597af 100644 --- a/assets/voxygen/shaders/figure-frag.glsl +++ b/assets/voxygen/shaders/figure-frag.glsl @@ -1,5 +1,7 @@ #version 330 core +#define FIGURE_SHADER + #include #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; diff --git a/assets/voxygen/shaders/figure-vert.glsl b/assets/voxygen/shaders/figure-vert.glsl index 50acb8e4eb..021b319a11 100644 --- a/assets/voxygen/shaders/figure-vert.glsl +++ b/assets/voxygen/shaders/figure-vert.glsl @@ -2,6 +2,8 @@ #include +#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 diff --git a/assets/voxygen/shaders/include/light.glsl b/assets/voxygen/shaders/include/light.glsl index 8aabebd6ec..e30c1fc052 100644 --- a/assets/voxygen/shaders/include/light.glsl +++ b/assets/voxygen/shaders/include/light.glsl @@ -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; diff --git a/assets/voxygen/shaders/include/random.glsl b/assets/voxygen/shaders/include/random.glsl index 9a5432cb1a..6f337fee35 100644 --- a/assets/voxygen/shaders/include/random.glsl +++ b/assets/voxygen/shaders/include/random.glsl @@ -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; diff --git a/assets/voxygen/shaders/include/sky.glsl b/assets/voxygen/shaders/include/sky.glsl index a65962c099..b614efbd7f 100644 --- a/assets/voxygen/shaders/include/sky.glsl +++ b/assets/voxygen/shaders/include/sky.glsl @@ -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); diff --git a/assets/voxygen/shaders/light-shadows-figure-vert.glsl b/assets/voxygen/shaders/light-shadows-figure-vert.glsl index 154cc23b88..2178cae73e 100644 --- a/assets/voxygen/shaders/light-shadows-figure-vert.glsl +++ b/assets/voxygen/shaders/light-shadows-figure-vert.glsl @@ -1,6 +1,8 @@ #version 330 core // #extension ARB_texture_storage : enable +#define FIGURE_SHADER + #include #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 diff --git a/assets/voxygen/shaders/lod-terrain-frag.glsl b/assets/voxygen/shaders/lod-terrain-frag.glsl index d53eba5abe..defb468ec7 100644 --- a/assets/voxygen/shaders/lod-terrain-frag.glsl +++ b/assets/voxygen/shaders/lod-terrain-frag.glsl @@ -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); diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl index 7bf80b19c5..e07c0b90b9 100644 --- a/assets/voxygen/shaders/particle-vert.glsl +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -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) { diff --git a/assets/voxygen/shaders/player-shadow-frag.glsl b/assets/voxygen/shaders/player-shadow-frag.glsl index dd28aaba34..d64be8f67e 100644 --- a/assets/voxygen/shaders/player-shadow-frag.glsl +++ b/assets/voxygen/shaders/player-shadow-frag.glsl @@ -1,5 +1,7 @@ #version 330 core +#define FIGURE_SHADER + #include #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; diff --git a/assets/voxygen/shaders/sprite-frag.glsl b/assets/voxygen/shaders/sprite-frag.glsl index 5295423998..317f385417 100644 --- a/assets/voxygen/shaders/sprite-frag.glsl +++ b/assets/voxygen/shaders/sprite-frag.glsl @@ -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; diff --git a/assets/voxygen/shaders/terrain-frag.glsl b/assets/voxygen/shaders/terrain-frag.glsl index d81466ea6c..16863e36eb 100644 --- a/assets/voxygen/shaders/terrain-frag.glsl +++ b/assets/voxygen/shaders/terrain-frag.glsl @@ -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); diff --git a/assets/voxygen/voxel/sprite/lantern/lantern-orange.vox b/assets/voxygen/voxel/sprite/lantern/lantern-orange.vox new file mode 100644 index 0000000000..104ccaec39 --- /dev/null +++ b/assets/voxygen/voxel/sprite/lantern/lantern-orange.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:267dad1374260e2e072027ba005389ba66082f74e7c8ba64a83bb58d41a04fc3 +size 1860 diff --git a/assets/voxygen/voxel/sprite_manifest.ron b/assets/voxygen/voxel/sprite_manifest.ron index c4aff9d909..158bb5e386 100644 --- a/assets/voxygen/voxel/sprite_manifest.ron +++ b/assets/voxygen/voxel/sprite_manifest.ron @@ -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, +)), ) diff --git a/common/net/src/msg/world_msg.rs b/common/net/src/msg/world_msg.rs index e0421da899..4a11f715b4 100644 --- a/common/net/src/msg/world_msg.rs +++ b/common/net/src/msg/world_msg.rs @@ -138,4 +138,5 @@ pub enum SiteKind { Dungeon { difficulty: u32 }, Castle, Cave, + Tree, } diff --git a/common/src/path.rs b/common/src/path.rs index d8b475dad5..0743191b0c 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -32,6 +32,13 @@ impl FromIterator for Path { } } +impl IntoIterator for Path { + type IntoIter = std::vec::IntoIter; + type Item = T; + + fn into_iter(self) -> Self::IntoIter { self.nodes.into_iter() } +} + impl Path { pub fn is_empty(&self) -> bool { self.nodes.is_empty() } diff --git a/common/src/store.rs b/common/src/store.rs index e3ededbd25..75ce9dc427 100644 --- a/common/src/store.rs +++ b/common/src/store.rs @@ -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(u64, PhantomData); +pub struct Id { + idx: u32, + gen: u32, + phantom: PhantomData, +} impl Id { - pub fn id(&self) -> u64 { self.0 } + pub fn id(&self) -> u64 { self.idx as u64 | ((self.gen as u64) << 32) } } impl Copy for Id {} impl Clone for Id { - fn clone(&self) -> Self { Self(self.0, PhantomData) } + fn clone(&self) -> Self { + Self { + idx: self.idx, + gen: self.gen, + phantom: PhantomData, + } + } } impl Eq for Id {} impl PartialEq for Id { - 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 Ord for Id { + fn cmp(&self, other: &Self) -> Ordering { (self.idx, self.gen).cmp(&(other.idx, other.gen)) } +} +impl PartialOrd for Id { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl fmt::Debug for Id { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Id<{}>({})", std::any::type_name::(), self.0) + write!( + f, + "Id<{}>({}, {})", + std::any::type_name::(), + self.idx, + self.gen + ) } } impl hash::Hash for Id { - fn hash(&self, h: &mut H) { self.0.hash(h); } + fn hash(&self, h: &mut H) { + self.idx.hash(h); + self.gen.hash(h); + } +} + +struct Entry { + gen: u32, + item: Option, } pub struct Store { - items: Vec, + entries: Vec>, + len: usize, } impl Default for Store { - fn default() -> Self { Self { items: Vec::new() } } + fn default() -> Self { + Self { + entries: Vec::new(), + len: 0, + } + } } impl Store { + pub fn is_empty(&self) -> bool { self.len == 0 } + + pub fn len(&self) -> usize { self.len } + + pub fn contains(&self, id: Id) -> bool { + self.entries + .get(id.idx as usize) + .map(|e| e.gen == id.gen) + .unwrap_or(false) + } + pub fn get(&self, id: Id) -> &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) -> &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> { - (0..self.items.len()).map(|i| Id(i as u64, PhantomData)) + pub fn ids(&self) -> impl Iterator> + '_ { self.iter().map(|(id, _)| id) } + + pub fn values(&self) -> impl Iterator + '_ { self.iter().map(|(_, item)| item) } + + pub fn values_mut(&mut self) -> impl Iterator + '_ { + self.iter_mut().map(|(_, item)| item) } - pub fn values(&self) -> impl Iterator { self.items.iter() } + pub fn iter(&self) -> impl Iterator, &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 { self.items.iter_mut() } - - pub fn iter(&self) -> impl Iterator, &T)> { self.ids().zip(self.values()) } - - pub fn iter_mut(&mut self) -> impl Iterator, &mut T)> { - self.ids().zip(self.values_mut()) + pub fn iter_mut(&mut self) -> impl Iterator, &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 { - // 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) -> Option { + 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 + } } } diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 5da34b509f..74fe3e852f 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -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() diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index de7e8fd6ff..bf67e20c43 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -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 ) } } diff --git a/common/src/terrain/structure.rs b/common/src/terrain/structure.rs index 57235240c5..c9b12d8d0c 100644 --- a/common/src/terrain/structure.rs +++ b/common/src/terrain/structure.rs @@ -30,11 +30,15 @@ make_case_elim!( Hollow = 13, Liana = 14, Normal(color: Rgb) = 15, + Log = 16, + Block(kind: BlockKind, color: Rgb) = 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, - empty: StructureBlock, default_kind: BlockKind, } @@ -109,7 +112,7 @@ impl ReadVol for Structure { fn get(&self, pos: Vec3) -> 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, }) } diff --git a/common/src/volumes/vol_grid_2d.rs b/common/src/volumes/vol_grid_2d.rs index 4432fc80a5..03ecec38af 100644 --- a/common/src/volumes/vol_grid_2d.rs +++ b/common/src/volumes/vol_grid_2d.rs @@ -26,7 +26,7 @@ impl VolGrid2d { #[inline(always)] pub fn chunk_key>>(pos: P) -> Vec2 { 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)] diff --git a/voxygen/anim/src/vek.rs b/voxygen/anim/src/vek.rs index ee9bda3c45..1d00e7cd4e 100644 --- a/voxygen/anim/src/vek.rs +++ b/voxygen/anim/src/vek.rs @@ -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::*, -}; */ +};*/ diff --git a/voxygen/src/hud/img_ids.rs b/voxygen/src/hud/img_ids.rs index 1d4cc01cfc..c63747e245 100644 --- a/voxygen/src/hud/img_ids.rs +++ b/voxygen/src/hud/img_ids.rs @@ -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", diff --git a/voxygen/src/hud/map.rs b/voxygen/src/hud/map.rs index d22d920408..f7a6c57444 100644 --- a/voxygen/src/hud/map.rs +++ b/voxygen/src/hud/map.rs @@ -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) + } + }, } } } diff --git a/voxygen/src/hud/minimap.rs b/voxygen/src/hud/minimap.rs index 3162bb6681..ab0d64c9eb 100644 --- a/voxygen/src/hud/minimap.rs +++ b/voxygen/src/hud/minimap.rs @@ -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) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 2b4d5af1b4..7efda93209 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -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 { diff --git a/voxygen/src/mesh/terrain.rs b/voxygen/src/mesh/terrain.rs index 242ee59fed..d2ab578a6e 100644 --- a/voxygen/src/mesh/terrain.rs +++ b/voxygen/src/mesh/terrain.rs @@ -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 + ReadVol + Debug>( is_sunlight: bool, @@ -69,25 +69,24 @@ fn calc_light + 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 + 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 + 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| light(pos + range.min); + let get_light = |_: &mut (), pos: Vec3| { + if flat_get(pos).is_opaque() { + 0.0 + } else { + light(pos + range.min) + } + }; let get_glow = |_: &mut (), pos: Vec3| glow(pos + range.min); let get_color = |_: &mut (), pos: Vec3| flat_get(pos).get_color().unwrap_or(Rgb::zero()); diff --git a/voxygen/src/render/pipelines/figure.rs b/voxygen/src/render/pipelines/figure.rs index 662a44953f..d9f5f45b74 100644 --- a/voxygen/src/render/pipelines/figure.rs +++ b/voxygen/src/render/pipelines/figure.rs @@ -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, is_player: bool, light: f32, - glow: f32, + glow: (Vec3, 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), ) } } diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 1780fa7f8f..e8821425e2 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -4602,7 +4602,7 @@ pub struct FigureStateMeta { last_pos: Option>, avg_vel: anim::vek::Vec3, last_light: f32, - last_glow: f32, + last_glow: (Vec3, f32), acc_vel: f32, } @@ -4649,7 +4649,7 @@ impl FigureState { 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 FigureState { 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, diff --git a/voxygen/src/scene/math.rs b/voxygen/src/scene/math.rs index 7f21fac5f5..dcd43c4efd 100644 --- a/voxygen/src/scene/math.rs +++ b/voxygen/src/scene/math.rs @@ -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(bounds: Aabb) -> [Vec3; 8] { diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index efd3d3de8e..d7d1f658bf 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -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, diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index 4b6aeb7d44..725141cb1a 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -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 Terrain { .unwrap_or(0.0) } + pub fn glow_normal_at_wpos(&self, wpos: Vec3) -> (Vec3, 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::::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 Terrain { // 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 Terrain { // 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 diff --git a/voxygen/src/scene/terrain/watcher.rs b/voxygen/src/scene/terrain/watcher.rs index 257e01c05e..2d68b512d5 100644 --- a/voxygen/src/scene/terrain/watcher.rs +++ b/voxygen/src/scene/terrain/watcher.rs @@ -15,6 +15,7 @@ pub struct BlocksOfInterest { pub smokers: Vec>, pub beehives: Vec>, pub reeds: Vec>, + pub fireflies: Vec>, pub flowers: Vec>, pub fire_bowls: Vec>, pub snow: Vec>, @@ -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, diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index a7e863c183..4344a54162 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -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(); diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index 3bea161074..fc33b4007d 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -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, } diff --git a/world/Cargo.toml b/world/Cargo.toml index 1ebf3f4866..ca3d12b491 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -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" diff --git a/world/benches/tree.rs b/world/benches/tree.rs new file mode 100644 index 0000000000..291c30e792 --- /dev/null +++ b/world/benches/tree.rs @@ -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); diff --git a/world/examples/site.rs b/world/examples/site.rs new file mode 100644 index 0000000000..a731dcdc52 --- /dev/null +++ b/world/examples/site.rs @@ -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); +} diff --git a/world/src/all.rs b/world/src/all.rs index fb44983678..030952d731 100644 --- a/world/src/all.rs +++ b/world/src/all.rs @@ -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 { + 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 { + 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> { + 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, + pub seed: u32, + pub scale: f32, + pub forest_kind: ForestKind, + pub inhabited: bool, +} diff --git a/world/src/block/mod.rs b/world/src/block/mod.rs index a7f3c4c156..a1284ac22e 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -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 diff --git a/world/src/canvas.rs b/world/src/canvas.rs index d6ed1acd14..f7a8b22300 100644 --- a/world/src/canvas.rs +++ b/world/src/canvas.rs @@ -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, pub(crate) column_grid: &'a Grid>>, 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) -> Option { - self.chunk.get(pos - self.wpos()).ok().copied() + pub fn get(&mut self, pos: Vec3) -> Block { + self.chunk + .get(pos - self.wpos()) + .ok() + .copied() + .unwrap_or_else(Block::empty) } pub fn set(&mut self, pos: Vec3, block: Block) { diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index cf791fef6e..628d1536c9 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -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, dir: Vec2) -> Option { /// Return true if a position is suitable for walking on fn loc_suitable_for_walking(sim: &WorldSim, loc: Vec2) -> 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 { diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 04aa0231fa..4e13675ff7 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -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, Path, Vec2)>, pub cave: Option<(f32, Vec2, Cave, Vec2)>, pub snow_cover: bool, + pub cliff_offset: f32, + pub cliff_height: f32, pub chunk: &'a SimChunk, } diff --git a/world/src/land.rs b/world/src/land.rs new file mode 100644 index 0000000000..8384251647 --- /dev/null +++ b/world/src/land.rs @@ -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) -> f32 { + self.sim + .and_then(|sim| sim.get_alt_approx(wpos)) + .unwrap_or(0.0) + } + + pub fn get_gradient_approx(&self, wpos: Vec2) -> 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) -> Option<&sim::SimChunk> { + self.sim.and_then(|sim| sim.get_wpos(wpos)) + } +} diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 486fe3bd9a..24c38c3b6a 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -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 = 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)), + ); + } + } + }); +} diff --git a/world/src/layer/scatter.rs b/world/src/layer/scatter.rs index b1a153d6d9..3c3d03ab1b 100644 --- a/world/src/layer/scatter.rs +++ b/world/src/layer/scatter.rs @@ -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() }) }) { diff --git a/world/src/layer/tree.rs b/world/src/layer/tree.rs index ee1e9ba8f3..d94c8e417a 100644 --- a/world/src/layer/tree.rs +++ b/world/src/layer/tree.rs @@ -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, - model: Structure, + model: TreeModel, seed: u32, units: (Vec2, Vec2), } @@ -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, + /// 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, + /// 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, + /// 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::().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::().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, + 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, + dir: Vec3, + branch_len: f32, + branch_radius: f32, + depth: usize, + sibling_idx: Option, + rng: &mut impl Rng, + ) -> (usize, Aabb) { + 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::::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::::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 { 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, + 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::::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::::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) -> (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, + wood_radius: f32, + leaf_radius: f32, + leaf_vertical_scale: f32, + aabb: Aabb, + + sibling_idx: Option, + child_idx: Option, + + 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, + 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, p: Vec3) -> 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) + } +} diff --git a/world/src/lib.rs b/world/src/lib.rs index 859f829d82..0117d899a3 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -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 diff --git a/world/src/sim/map.rs b/world/src/sim/map.rs index 59b9a0f947..8ac6747b97 100644 --- a/world/src/sim/map.rs +++ b/world/src/sim/map.rs @@ -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. diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index 7d9d4c1590..df95fa4ee2 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -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) -> impl Iterator, u32)> + '_ { + pub fn get_near_trees(&self, wpos: Vec2) -> impl Iterator + '_ { // 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::>(), + ) + .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>, @@ -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 } } diff --git a/world/src/sim2/mod.rs b/world/src/sim2/mod.rs index a89d327d0c..2216286ed2 100644 --- a/world/src/sim2/mod.rs +++ b/world/src/sim2/mod.rs @@ -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::>(); + for site in site_ids { tick_site_economy(index, site, dt); } diff --git a/world/src/site/mod.rs b/world/src/site/mod.rs index da168c28f5..2f7ed2a7e3 100644 --- a/world/src/site/mod.rs +++ b/world/src/site/mod.rs @@ -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, - get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, - vol: &mut (impl BaseVol + 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(_) => {}, } } } diff --git a/world/src/site/tree.rs b/world/src/site/tree.rs new file mode 100644 index 0000000000..c1c8327cd4 --- /dev/null +++ b/world/src/site/tree.rs @@ -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, + alt: i32, + seed: u32, + tree: ProceduralTree, +} + +impl Tree { + pub fn generate(origin: Vec2, 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) -> 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; + } + } + }); + } +} diff --git a/world/src/site2/gen.rs b/world/src/site2/gen.rs new file mode 100644 index 0000000000..af81fbec7a --- /dev/null +++ b/world/src/site2/gen.rs @@ -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), + Pyramid { aabb: Aabb, inset: i32 }, + + // Combinators + And(Id, Id), + Or(Id, Id), + Xor(Id, Id), +} + +pub enum Fill { + Block(Block), + Brick(BlockKind, Rgb, u8), +} + +impl Fill { + fn contains_at(&self, tree: &Store, prim: Id, pos: Vec3) -> bool { + // Custom closure because vek's impl of `contains_point` is inclusive :( + let aabb_contains = |aabb: Aabb, pos: Vec3| { + (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, + prim: Id, + pos: Vec3, + ) -> Option { + 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, prim: Id) -> Option> { + fn or_zip_with T>(a: Option, b: Option, f: F) -> Option { + 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, prim: Id) -> Aabb { + self.get_bounds_inner(tree, prim) + .unwrap_or_else(|| Aabb::new_empty(Vec3::zero())) + } +} + +pub trait Structure { + fn render Id, G: FnMut(Id, Fill)>( + &self, + site: &Site, + prim: F, + fill: G, + ); + + // Generate a primitive tree and fills for this structure + fn render_collect(&self, site: &Site) -> (Store, Vec<(Id, 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) + } +} diff --git a/world/src/site2/mod.rs b/world/src/site2/mod.rs new file mode 100644 index 0000000000..4587ad2e4f --- /dev/null +++ b/world/src/site2/mod.rs @@ -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, + tiles: TileGrid, + plots: Store, + plazas: Vec>, + roads: Vec>, +} + +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) -> 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 { + 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 { &self.plots[id] } + + pub fn plots(&self) -> impl Iterator + '_ { self.plots.values() } + + pub fn create_plot(&mut self, plot: Plot) -> Id { self.plots.insert(plot) } + + pub fn blit_aabr(&mut self, aabr: Aabr, 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, + b: Vec2, + w: u16, + ) -> Option> { + const MAX_ITERS: usize = 4096; + let range = -(w as i32) / 2..w as i32 - (w as i32 + 1) / 2; + let heuristic = |tile: &Vec2| { + 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, + area_range: Range, + min_dims: Extent2, + ) -> Option<(Aabr, Vec2)> { + 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, + min_dims: Extent2, + ) -> Option<(Aabr, Vec2)> { + let dir = Vec2::::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 { + 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) -> 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::().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) -> Vec2 { + (wpos2d - self.origin).map(|e| e.div_euclid(TILE_SIZE as i32)) + } + + pub fn wpos_tile(&self, wpos2d: Vec2) -> &Tile { + self.tiles.get(self.wpos_tile_pos(wpos2d)) + } + + pub fn tile_wpos(&self, tile: Vec2) -> Vec2 { + self.origin + tile * tile::TILE_SIZE as i32 + } + + pub fn tile_center_wpos(&self, tile: Vec2) -> Vec2 { + 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) { + 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::>(); + 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) -> Option { + 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) -> impl Iterator> { + (0..aabr.size().h) + .map(move |y| (0..aabr.size().w).map(move |x| aabr.min + Vec2::new(x, y))) + .flatten() +} + +pub struct Plaza {} diff --git a/world/src/site2/plot.rs b/world/src/site2/plot.rs new file mode 100644 index 0000000000..3b0bcaa1f1 --- /dev/null +++ b/world/src/site2/plot.rs @@ -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, + pub(crate) tiles: DHashSet>, + pub(crate) seed: u32, +} + +impl Plot { + pub fn find_bounds(&self) -> Aabr { + 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>), +} diff --git a/world/src/site2/plot/castle.rs b/world/src/site2/plot/castle.rs new file mode 100644 index 0000000000..6a185428c5 --- /dev/null +++ b/world/src/site2/plot/castle.rs @@ -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, + tile_aabr: Aabr, + _bounds: Aabr, + alt: i32, +} + +impl Castle { + pub fn generate( + land: &Land, + _rng: &mut impl Rng, + site: &Site, + entrance_tile: Vec2, + tile_aabr: Aabr, + ) -> 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 Id, G: FnMut(Id, 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) + .with_z(self.alt + wall_height), + max: (wpos + + (6 - parapet_width) * ori as Vec2 + + 6 * ori.yx() as Vec2) + .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; + } + }, + } + }, + _ => {}, + } + } + } + } +} diff --git a/world/src/site2/plot/house.rs b/world/src/site2/plot/house.rs new file mode 100644 index 0000000000..7bc85fc8c0 --- /dev/null +++ b/world/src/site2/plot/house.rs @@ -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, + tile_aabr: Aabr, + bounds: Aabr, + alt: i32, + levels: u32, + roof_color: Rgb, +} + +impl House { + pub fn generate( + land: &Land, + rng: &mut impl Rng, + site: &Site, + door_tile: Vec2, + tile_aabr: Aabr, + ) -> 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 Id, G: FnMut(Id, 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))), + ); + } +} diff --git a/world/src/site2/tile.rs b/world/src/site2/tile.rs new file mode 100644 index 0000000000..ef757f9887 --- /dev/null +++ b/world/src/site2/tile.rs @@ -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, // Inclusive + zones: Grid>>>, +} + +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) -> &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) -> 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, tile: Tile) -> Option { + self.bounds.expand_to_contain_point(tpos); + self.get_mut(tpos).map(|t| std::mem::replace(t, tile)) + } + + pub fn find_near( + &self, + tpos: Vec2, + mut f: impl FnMut(Vec2, &Tile) -> Option, + ) -> Option<(R, Vec2)> { + 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, + area_range: Range, + min_dims: Extent2, + ) -> Result, Aabr> { + 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, + area_range: Range, + ) -> Result>, DHashSet>> { + 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>, +} + +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 { CARDINALS[self as u8 as usize] } +} diff --git a/world/src/util/fast_noise.rs b/world/src/util/fast_noise.rs index 8caa007639..84ac0dc87a 100644 --- a/world/src/util/fast_noise.rs +++ b/world/src/util/fast_noise.rs @@ -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) -> f32 { + (self.noise.get(Vec3::new(pos.x, pos.y, 0)) & 4095) as f32 * 0.000244140625 + } +} + +impl Sampler<'static> for FastNoise2d { + type Index = Vec2; + 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 + } +} diff --git a/world/src/util/math.rs b/world/src/util/math.rs new file mode 100644 index 0000000000..56f91d795a --- /dev/null +++ b/world/src/util/math.rs @@ -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 { + 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) +} diff --git a/world/src/util/mod.rs b/world/src/util/mod.rs index 8d20dc2216..0a22405bcd 100644 --- a/world/src/util/mod.rs +++ b/world/src/util/mod.rs @@ -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; 5] = [ Vec2::new(0, -1), Vec2::new(-1, 0), ]; + +pub const SQUARE_4: [Vec2; 4] = [ + Vec2::new(0, 0), + Vec2::new(1, 0), + Vec2::new(0, 1), + Vec2::new(1, 1), +]; + +pub const SQUARE_9: [Vec2; 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), +]; diff --git a/world/src/util/random.rs b/world/src/util/random.rs index 09c7a815cb..2e85f2f978 100644 --- a/world/src/util/random.rs +++ b/world/src/util/random.rs @@ -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 }