From 5db5eec575aebb950c08819157795934cafae229 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 3 Feb 2026 22:13:22 -0800 Subject: [PATCH 1/7] Elevation gradient propogation, with no random perturbations yet --- common/src/math.rs | 7 +++ common/src/worldgen/horosphere.rs | 2 +- common/src/worldgen/mod.rs | 96 +++++++++++++++++++++++++++++-- 3 files changed, 99 insertions(+), 6 deletions(-) diff --git a/common/src/math.rs b/common/src/math.rs index a4a0bb68..b6fa4966 100644 --- a/common/src/math.rs +++ b/common/src/math.rs @@ -507,6 +507,13 @@ impl MDirection { pub fn cast>(self) -> MDirection { MDirection(self.0.cast()) } + + /// Project to be orthogonal to the origin [0 0 0 1] + #[inline] + pub fn project_to_origin(&self) -> Self { + let p = MPoint::origin(); + (self.as_ref() + p.as_ref() * self.mip(&p)).normalized_direction() + } } impl From> for na::Vector4 { diff --git a/common/src/worldgen/horosphere.rs b/common/src/worldgen/horosphere.rs index e9433927..2cce01ef 100644 --- a/common/src/worldgen/horosphere.rs +++ b/common/src/worldgen/horosphere.rs @@ -17,7 +17,7 @@ use crate::{ /// Whether an assortment of random horospheres should be added to world generation. This is a temporary /// option until large structures that fit with the theme of the world are introduced. /// For code simplicity, this is made into a constant instead of a configuration option. -const HOROSPHERES_ENABLED: bool = true; +const HOROSPHERES_ENABLED: bool = false; /// Value to mix into the node's spice for generating horospheres. Chosen randomly. const HOROSPHERE_SEED: u64 = 6046133366614030452; diff --git a/common/src/worldgen/mod.rs b/common/src/worldgen/mod.rs index 17ef4111..a2fab212 100644 --- a/common/src/worldgen/mod.rs +++ b/common/src/worldgen/mod.rs @@ -1,4 +1,5 @@ use horosphere::{HorosphereChunk, HorosphereNode}; +use na::Vector4; use plane::Plane; use rand::{Rng, SeedableRng, distr::Uniform}; use rand_distr::Normal; @@ -8,7 +9,7 @@ use crate::{ dodeca::{Side, Vertex}, graph::{Graph, NodeId}, margins, - math::{self, MVector}, + math::{self, MDirection, MPoint, MVector}, node::{ChunkId, VoxelData}, world::Material, }; @@ -112,10 +113,11 @@ impl NodeState { temperature: 0.0, rainfall: 0.0, blockiness: 0.0, + elevation_gradient: EnviroGradient::new_from_direction(3.0_f32, *Side::B.normal()), }, (Some(parent), None) => { let spice = graph.hash_of(node) as u64; - EnviroFactors::varied_from(parent.node_state.enviro, spice) + EnviroFactors::varied_from(parent.node_state.enviro, parent.side, spice) } (Some(parent_a), Some(parent_b)) => { let ab_node = graph.neighbor(parent_a.node_id, parent_b.side).unwrap(); @@ -124,6 +126,8 @@ impl NodeState { parent_a.node_state.enviro, parent_b.node_state.enviro, ab_state.enviro, + parent_a.side, + parent_b.side, ) } _ => unreachable!(), @@ -530,32 +534,114 @@ struct NeighborData { material: Material, } +#[derive(Copy, Clone)] +struct EnviroGradient { + magnitude: f32, + direction: MDirection, +} +impl EnviroGradient { + fn new(magnitude: f32, vector: MVector) -> Self { + Self { + magnitude, + direction: vector.normalized_direction().project_to_origin(), + } + } + + fn new_from_direction(magnitude: f32, direction: MDirection) -> Self { + Self { + magnitude, + direction: direction.project_to_origin(), + } + } + + fn reflect_from_side(&self, side: Side) -> Self { + // reflect, then drop the w-component and renormalize + Self { + magnitude: self.magnitude, + direction: (side.reflection() * self.direction).project_to_origin(), + } + } + + // Crude "average" between two gradients + fn average(&self, other: &EnviroGradient) -> Self { + let v1 = Vector4::from(self.direction); + let v2 = Vector4::from(other.direction); + Self { + magnitude: 0.5 * (self.magnitude + other.magnitude), + direction: MVector::from(v1 + v2).normalized_direction(), + } + } +} + #[derive(Copy, Clone)] struct EnviroFactors { max_elevation: f32, temperature: f32, rainfall: f32, blockiness: f32, + elevation_gradient: EnviroGradient, } impl EnviroFactors { - fn varied_from(parent: Self, spice: u64) -> Self { + fn varied_from(parent: Self, side: Side, spice: u64) -> Self { let mut rng = rand_pcg::Pcg64Mcg::seed_from_u64(spice); let unif = Uniform::new_inclusive(-1.0, 1.0).unwrap(); - let max_elevation = parent.max_elevation + rng.sample(Normal::new(0.0, 4.0).unwrap()); + + let elevation_gradient = parent.elevation_gradient.reflect_from_side(side); // No pertubations for now + let average_gradient = parent + .elevation_gradient + .average(&elevation_gradient.reflect_from_side(side)); + let traversal_direction = { + let p = MPoint::origin(); + let n = side.normal(); + (n.as_ref() + p.as_ref() * n.mip(&p)).normalized_direction() + }; + let parallel_component = average_gradient.direction.mip(&traversal_direction); + + // Increase elevation according to how perpendicular is to traversal direction + let max_elevation = parent.max_elevation + + average_gradient.magnitude * parallel_component + + rng.sample(Normal::new(0.0, 0.01).unwrap()); Self { max_elevation, temperature: parent.temperature + rng.sample(unif), rainfall: parent.rainfall + rng.sample(unif), blockiness: parent.blockiness + rng.sample(unif), + elevation_gradient, } } - fn continue_from(a: Self, b: Self, ab: Self) -> Self { + fn continue_from(a: Self, b: Self, ab: Self, side_a: Side, side_b: Side) -> Self { + // project the gradients of a, b, and ab into the new node's frame of reference, + // then the new gradient's direction is calculated as directions of (a + (b - ab). + // This process doesn't really have a physical meaning, but it's an isotropic way for + // the child node's gradient direction to be inherited, and it's intuitive for it to operate + // in a similar fashion to the scalar environmental factors. + let gradient_direction = (a + .elevation_gradient + .reflect_from_side(side_a) + .direction + .as_ref() + + (b.elevation_gradient + .reflect_from_side(side_b) + .direction + .as_ref() + - ab.elevation_gradient + .reflect_from_side(side_a) + .reflect_from_side(side_b) + .direction + .as_ref())) + .normalized_direction(); + let gradient_magnitude = a.elevation_gradient.magnitude + + (b.elevation_gradient.magnitude - ab.elevation_gradient.magnitude); Self { max_elevation: a.max_elevation + (b.max_elevation - ab.max_elevation), temperature: a.temperature + (b.temperature - ab.temperature), rainfall: a.rainfall + (b.rainfall - ab.rainfall), blockiness: a.blockiness + (b.blockiness - ab.blockiness), + elevation_gradient: EnviroGradient::new_from_direction( + gradient_magnitude, + gradient_direction, + ), } } } From 9c4e20bd47c702b47564ff7eb2533f06b5f15e0f Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 3 Feb 2026 22:15:08 -0800 Subject: [PATCH 2/7] mend --- common/src/worldgen/mod.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/common/src/worldgen/mod.rs b/common/src/worldgen/mod.rs index a2fab212..f1dfde29 100644 --- a/common/src/worldgen/mod.rs +++ b/common/src/worldgen/mod.rs @@ -540,13 +540,6 @@ struct EnviroGradient { direction: MDirection, } impl EnviroGradient { - fn new(magnitude: f32, vector: MVector) -> Self { - Self { - magnitude, - direction: vector.normalized_direction().project_to_origin(), - } - } - fn new_from_direction(magnitude: f32, direction: MDirection) -> Self { Self { magnitude, From d811fb7ae00e3f8a516c7f069a2b01a6248da724 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 4 Feb 2026 13:00:34 -0800 Subject: [PATCH 3/7] small random rotations of gradient --- common/src/worldgen/mod.rs | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/common/src/worldgen/mod.rs b/common/src/worldgen/mod.rs index f1dfde29..9fcc7889 100644 --- a/common/src/worldgen/mod.rs +++ b/common/src/worldgen/mod.rs @@ -2,7 +2,7 @@ use horosphere::{HorosphereChunk, HorosphereNode}; use na::Vector4; use plane::Plane; use rand::{Rng, SeedableRng, distr::Uniform}; -use rand_distr::Normal; +use rand_distr::{Normal, UnitSphere}; use terraingen::VoronoiInfo; use crate::{ @@ -555,7 +555,7 @@ impl EnviroGradient { } } - // Crude "average" between two gradients + /// Linear "average" between two gradients fn average(&self, other: &EnviroGradient) -> Self { let v1 = Vector4::from(self.direction); let v2 = Vector4::from(other.direction); @@ -564,6 +564,26 @@ impl EnviroGradient { direction: MVector::from(v1 + v2).normalized_direction(), } } + + /// Linearally mix the gradient's direction with another direction. + /// The mix is biased toward the origonal direction. + /// The intent is to use this with a randomly-generated direction. + /// ratio is the ratio of origonal direction's weight to perturbance direction's weight. + fn perturb(&self, perturbance_direction: MDirection, ratio: f32) -> Self { + // Allowing the perturbance to equal the origonal opens the door for division by zero + // during normalization. Consider using rotations or making use of smaller-scale noise + // if you need a stronger effect. + assert!(ratio > 1.0); + assert!(perturbance_direction.w == na::zero()); + + let v1 = Vector4::from(self.direction) * ratio; + let v2 = Vector4::from(perturbance_direction); + + Self { + magnitude: self.magnitude, + direction: MVector::from(v1 + v2).normalized_direction(), + } + } } #[derive(Copy, Clone)] @@ -579,7 +599,13 @@ impl EnviroFactors { let mut rng = rand_pcg::Pcg64Mcg::seed_from_u64(spice); let unif = Uniform::new_inclusive(-1.0, 1.0).unwrap(); - let elevation_gradient = parent.elevation_gradient.reflect_from_side(side); // No pertubations for now + let elevation_gradient = parent.elevation_gradient.reflect_from_side(side).perturb( + { + let v: [f32; 3] = rng.sample(UnitSphere); + MDirection::::new_unchecked(v[0], v[1], v[2], 0.0) + }, + 5.0, + ); let average_gradient = parent .elevation_gradient .average(&elevation_gradient.reflect_from_side(side)); @@ -593,7 +619,7 @@ impl EnviroFactors { // Increase elevation according to how perpendicular is to traversal direction let max_elevation = parent.max_elevation + average_gradient.magnitude * parallel_component - + rng.sample(Normal::new(0.0, 0.01).unwrap()); + + rng.sample(Normal::new(0.0, 0.1).unwrap()); Self { max_elevation, From c81ec1fc7aa6c15c63f7ac3783ea1fdd0c039bd2 Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 4 Feb 2026 14:35:40 -0800 Subject: [PATCH 4/7] fluctuate gradient magnitude --- common/src/worldgen/mod.rs | 38 +++++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/common/src/worldgen/mod.rs b/common/src/worldgen/mod.rs index 9fcc7889..d0b33fce 100644 --- a/common/src/worldgen/mod.rs +++ b/common/src/worldgen/mod.rs @@ -113,7 +113,7 @@ impl NodeState { temperature: 0.0, rainfall: 0.0, blockiness: 0.0, - elevation_gradient: EnviroGradient::new_from_direction(3.0_f32, *Side::B.normal()), + elevation_gradient: EnviroGradient::new_from_direction(1.5_f32, *Side::B.normal()), }, (Some(parent), None) => { let spice = graph.hash_of(node) as u64; @@ -534,6 +534,9 @@ struct NeighborData { material: Material, } +const ELEVATION_GRADIENT_MAGNITUDE_FLOOR: f32 = 0.0; +const ELEVATION_GRADIENT_MAGNITUDE_CEILING: f32 = 3.0; // maximum 27.0 + #[derive(Copy, Clone)] struct EnviroGradient { magnitude: f32, @@ -569,7 +572,12 @@ impl EnviroGradient { /// The mix is biased toward the origonal direction. /// The intent is to use this with a randomly-generated direction. /// ratio is the ratio of origonal direction's weight to perturbance direction's weight. - fn perturb(&self, perturbance_direction: MDirection, ratio: f32) -> Self { + fn perturb( + &self, + perturbance_direction: MDirection, + ratio: f32, + magnitude_delta: f32, + ) -> Self { // Allowing the perturbance to equal the origonal opens the door for division by zero // during normalization. Consider using rotations or making use of smaller-scale noise // if you need a stronger effect. @@ -579,11 +587,30 @@ impl EnviroGradient { let v1 = Vector4::from(self.direction) * ratio; let v2 = Vector4::from(perturbance_direction); + let m = self.magnitude + magnitude_delta; + Self { - magnitude: self.magnitude, + magnitude: { + if m < ELEVATION_GRADIENT_MAGNITUDE_FLOOR { + ELEVATION_GRADIENT_MAGNITUDE_FLOOR - (m - ELEVATION_GRADIENT_MAGNITUDE_FLOOR) + } else if m > ELEVATION_GRADIENT_MAGNITUDE_CEILING { + ELEVATION_GRADIENT_MAGNITUDE_CEILING + - (m - ELEVATION_GRADIENT_MAGNITUDE_CEILING) + } else { + m + } + }, direction: MVector::from(v1 + v2).normalized_direction(), } } + + /// Gets the gradient magnitude used for elevation delta calculations, + /// Which is the cube of the stored/propogated magnitude for more noticeable + /// terrain differences + #[inline] + fn get_gradient_magnitude(&self) -> f32 { + self.magnitude.powi(3) + } } #[derive(Copy, Clone)] @@ -604,7 +631,8 @@ impl EnviroFactors { let v: [f32; 3] = rng.sample(UnitSphere); MDirection::::new_unchecked(v[0], v[1], v[2], 0.0) }, - 5.0, + 10.0, + rng.sample(unif), ); let average_gradient = parent .elevation_gradient @@ -618,7 +646,7 @@ impl EnviroFactors { // Increase elevation according to how perpendicular is to traversal direction let max_elevation = parent.max_elevation - + average_gradient.magnitude * parallel_component + + average_gradient.get_gradient_magnitude() * parallel_component + rng.sample(Normal::new(0.0, 0.1).unwrap()); Self { From b5ec2940416e2d4da96cfe713f433f6677e1c6fe Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 4 Feb 2026 17:39:35 -0800 Subject: [PATCH 5/7] partial suppression of sky-island behavior --- common/src/worldgen/mod.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/common/src/worldgen/mod.rs b/common/src/worldgen/mod.rs index d0b33fce..97ae09e4 100644 --- a/common/src/worldgen/mod.rs +++ b/common/src/worldgen/mod.rs @@ -6,12 +6,7 @@ use rand_distr::{Normal, UnitSphere}; use terraingen::VoronoiInfo; use crate::{ - dodeca::{Side, Vertex}, - graph::{Graph, NodeId}, - margins, - math::{self, MDirection, MPoint, MVector}, - node::{ChunkId, VoxelData}, - world::Material, + dodeca::{Side, Vertex}, graph::{Graph, NodeId}, margins, math::{self, MDirection, MPoint, MVector}, node::{ChunkId, VoxelData}, world::Material }; mod horosphere; @@ -117,7 +112,13 @@ impl NodeState { }, (Some(parent), None) => { let spice = graph.hash_of(node) as u64; - EnviroFactors::varied_from(parent.node_state.enviro, parent.side, spice) + let (traversal_direction, up_direction) = { + let p = MPoint::origin(); + let t = parent.side.normal(); + let u = parent.node_state.up_direction(); + ((t.as_ref() + p.as_ref() * t.mip(&p)).normalized_direction(), (u.as_ref() + p.as_ref() * u.mip(&p)).normalized_direction()) + }; + EnviroFactors::varied_from(parent.node_state.enviro, parent.side, traversal_direction, up_direction, spice) } (Some(parent_a), Some(parent_b)) => { let ab_node = graph.neighbor(parent_a.node_id, parent_b.side).unwrap(); @@ -622,7 +623,7 @@ struct EnviroFactors { elevation_gradient: EnviroGradient, } impl EnviroFactors { - fn varied_from(parent: Self, side: Side, spice: u64) -> Self { + fn varied_from(parent: Self, side: Side, traversal_direction: MDirection, up_direction: MDirection, spice: u64) -> Self { let mut rng = rand_pcg::Pcg64Mcg::seed_from_u64(spice); let unif = Uniform::new_inclusive(-1.0, 1.0).unwrap(); @@ -637,16 +638,13 @@ impl EnviroFactors { let average_gradient = parent .elevation_gradient .average(&elevation_gradient.reflect_from_side(side)); - let traversal_direction = { - let p = MPoint::origin(); - let n = side.normal(); - (n.as_ref() + p.as_ref() * n.mip(&p)).normalized_direction() - }; + let parallel_component = average_gradient.direction.mip(&traversal_direction); + let sky_islands_suppression_factor = (1.0 - (traversal_direction.mip(&up_direction)).powi(2)).sqrt(); // Increase elevation according to how perpendicular is to traversal direction let max_elevation = parent.max_elevation - + average_gradient.get_gradient_magnitude() * parallel_component + + average_gradient.get_gradient_magnitude() * sky_islands_suppression_factor * parallel_component + rng.sample(Normal::new(0.0, 0.1).unwrap()); Self { From 60e040333c64624f36301fd15b52938a9d30441a Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 4 Feb 2026 18:15:33 -0800 Subject: [PATCH 6/7] A few more comments --- common/src/worldgen/mod.rs | 53 +++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/common/src/worldgen/mod.rs b/common/src/worldgen/mod.rs index 97ae09e4..bda4ccdc 100644 --- a/common/src/worldgen/mod.rs +++ b/common/src/worldgen/mod.rs @@ -6,7 +6,12 @@ use rand_distr::{Normal, UnitSphere}; use terraingen::VoronoiInfo; use crate::{ - dodeca::{Side, Vertex}, graph::{Graph, NodeId}, margins, math::{self, MDirection, MPoint, MVector}, node::{ChunkId, VoxelData}, world::Material + dodeca::{Side, Vertex}, + graph::{Graph, NodeId}, + margins, + math::{self, MDirection, MPoint, MVector}, + node::{ChunkId, VoxelData}, + world::Material, }; mod horosphere; @@ -116,9 +121,18 @@ impl NodeState { let p = MPoint::origin(); let t = parent.side.normal(); let u = parent.node_state.up_direction(); - ((t.as_ref() + p.as_ref() * t.mip(&p)).normalized_direction(), (u.as_ref() + p.as_ref() * u.mip(&p)).normalized_direction()) + ( + (t.as_ref() + p.as_ref() * t.mip(&p)).normalized_direction(), + (u.as_ref() + p.as_ref() * u.mip(&p)).normalized_direction(), + ) }; - EnviroFactors::varied_from(parent.node_state.enviro, parent.side, traversal_direction, up_direction, spice) + EnviroFactors::varied_from( + parent.node_state.enviro, + parent.side, + traversal_direction, + up_direction, + spice, + ) } (Some(parent_a), Some(parent_b)) => { let ab_node = graph.neighbor(parent_a.node_id, parent_b.side).unwrap(); @@ -539,6 +553,9 @@ const ELEVATION_GRADIENT_MAGNITUDE_FLOOR: f32 = 0.0; const ELEVATION_GRADIENT_MAGNITUDE_CEILING: f32 = 3.0; // maximum 27.0 #[derive(Copy, Clone)] +/// A gradient representing evironmental factors. +/// Consists of a magnitude and direction, which are for the most part operated on indpendently. +/// The magnitude can be scaled arbitrarily, for example the elevation gradient is on a cubic scale. struct EnviroGradient { magnitude: f32, direction: MDirection, @@ -573,6 +590,8 @@ impl EnviroGradient { /// The mix is biased toward the origonal direction. /// The intent is to use this with a randomly-generated direction. /// ratio is the ratio of origonal direction's weight to perturbance direction's weight. + /// Also linearly and independently adds a delta to the gradient magnitude, + /// which could certainly be a seperate function fn perturb( &self, perturbance_direction: MDirection, @@ -605,9 +624,9 @@ impl EnviroGradient { } } - /// Gets the gradient magnitude used for elevation delta calculations, - /// Which is the cube of the stored/propogated magnitude for more noticeable - /// terrain differences + /// Gets the gradient magnitude used for elevation delta calculations. + /// That magnitude is the cube of the stored/propogated magnitude for more noticeable + /// terrain differences. #[inline] fn get_gradient_magnitude(&self) -> f32 { self.magnitude.powi(3) @@ -623,7 +642,13 @@ struct EnviroFactors { elevation_gradient: EnviroGradient, } impl EnviroFactors { - fn varied_from(parent: Self, side: Side, traversal_direction: MDirection, up_direction: MDirection, spice: u64) -> Self { + fn varied_from( + parent: Self, + side: Side, + traversal_direction: MDirection, + up_direction: MDirection, + spice: u64, + ) -> Self { let mut rng = rand_pcg::Pcg64Mcg::seed_from_u64(spice); let unif = Uniform::new_inclusive(-1.0, 1.0).unwrap(); @@ -633,18 +658,22 @@ impl EnviroFactors { MDirection::::new_unchecked(v[0], v[1], v[2], 0.0) }, 10.0, - rng.sample(unif), + 0.0, //rng.sample(unif), ); let average_gradient = parent .elevation_gradient .average(&elevation_gradient.reflect_from_side(side)); - + + // Increase elevation according to how perpendicular the gradient is to traversal direction let parallel_component = average_gradient.direction.mip(&traversal_direction); - let sky_islands_suppression_factor = (1.0 - (traversal_direction.mip(&up_direction)).powi(2)).sqrt(); + // Don't increase elevation when moving vertically + let sky_islands_suppression_factor = + (1.0 - (traversal_direction.mip(&up_direction)).powi(2)).sqrt(); - // Increase elevation according to how perpendicular is to traversal direction let max_elevation = parent.max_elevation - + average_gradient.get_gradient_magnitude() * sky_islands_suppression_factor * parallel_component + + average_gradient.get_gradient_magnitude() + * sky_islands_suppression_factor + * parallel_component + rng.sample(Normal::new(0.0, 0.1).unwrap()); Self { From 0b5a89a1ddaab4973035cefa37d411079f7479ef Mon Sep 17 00:00:00 2001 From: = <=> Date: Wed, 4 Feb 2026 18:33:56 -0800 Subject: [PATCH 7/7] mend --- common/src/worldgen/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/worldgen/mod.rs b/common/src/worldgen/mod.rs index bda4ccdc..a4283bdc 100644 --- a/common/src/worldgen/mod.rs +++ b/common/src/worldgen/mod.rs @@ -658,7 +658,7 @@ impl EnviroFactors { MDirection::::new_unchecked(v[0], v[1], v[2], 0.0) }, 10.0, - 0.0, //rng.sample(unif), + rng.sample(unif), ); let average_gradient = parent .elevation_gradient