Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,13 @@ impl<N: RealField + Copy> MDirection<N> {
pub fn cast<N2: RealField + Copy + SupersetOf<N>>(self) -> MDirection<N2> {
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<N: Scalar> From<MDirection<N>> for na::Vector4<N> {
Expand Down
2 changes: 1 addition & 1 deletion common/src/worldgen/horosphere.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated change; please omit.


/// Value to mix into the node's spice for generating horospheres. Chosen randomly.
const HOROSPHERE_SEED: u64 = 6046133366614030452;
Expand Down
172 changes: 166 additions & 6 deletions common/src/worldgen/mod.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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::{
dodeca::{Side, Vertex},
graph::{Graph, NodeId},
margins,
math::{self, MVector},
math::{self, MDirection, MPoint, MVector},
node::{ChunkId, VoxelData},
world::Material,
};
Expand Down Expand Up @@ -112,10 +113,26 @@ impl NodeState {
temperature: 0.0,
rainfall: 0.0,
blockiness: 0.0,
elevation_gradient: EnviroGradient::new_from_direction(1.5_f32, *Side::B.normal()),
},
(Some(parent), None) => {
let spice = graph.hash_of(node) as u64;
EnviroFactors::varied_from(parent.node_state.enviro, spice)
let (traversal_direction, up_direction) = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we're expanding a small amount of information into a much larger amount before passing it all through a function call. Could we push all this down into EnviroFactors::varied_from? Maybe pass parent wholesale?

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();
Expand All @@ -124,6 +141,8 @@ impl NodeState {
parent_a.node_state.enviro,
parent_b.node_state.enviro,
ab_state.enviro,
parent_a.side,
parent_b.side,
)
}
_ => unreachable!(),
Expand Down Expand Up @@ -530,32 +549,173 @@ struct NeighborData {
material: Material,
}

const ELEVATION_GRADIENT_MAGNITUDE_FLOOR: f32 = 0.0;
const ELEVATION_GRADIENT_MAGNITUDE_CEILING: f32 = 3.0; // maximum 27.0
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this comment mean? Elaborate.


#[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<f32>,
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style nit: include an empty line between top-level blocks

impl EnviroGradient {
fn new_from_direction(magnitude: f32, direction: MDirection<f32>) -> 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(),
}
}

/// Linear "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(),
}
}

/// 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.
/// Also linearly and independently adds a delta to the gradient magnitude,
/// which could certainly be a seperate function
fn perturb(
&self,
perturbance_direction: MDirection<f32>,
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a rotation here and avoid the hazard?

// if you need a stronger effect.
assert!(ratio > 1.0);
assert!(perturbance_direction.w == na::zero());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than panicking, could we pick an arbitrary direction here?


let v1 = Vector4::from(self.direction) * ratio;
let v2 = Vector4::from(perturbance_direction);

let m = self.magnitude + magnitude_delta;

Self {
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
}
Comment on lines +614 to +621
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be simplified using checked_sub to combine the subtraction with the branching?

},
direction: MVector::from(v1 + v2).normalized_direction(),
}
}

/// 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)
}
}

#[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,
traversal_direction: MDirection<f32>,
up_direction: MDirection<f32>,
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).perturb(
{
let v: [f32; 3] = rng.sample(UnitSphere);
MDirection::<f32>::new_unchecked(v[0], v[1], v[2], 0.0)
},
10.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);
// Don't increase elevation when moving vertically
let sky_islands_suppression_factor =
(1.0 - (traversal_direction.mip(&up_direction)).powi(2)).sqrt();

let max_elevation = parent.max_elevation
+ average_gradient.get_gradient_magnitude()
* sky_islands_suppression_factor
* parallel_component
+ rng.sample(Normal::new(0.0, 0.1).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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@patowen can you comment on the significance of this pattern? I'm completely drawing a blank on why we don't e.g. just average everything together here, and on what exactly "ab" is and what this method for combining vectors looks like visually.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to take a close look at this. This seems similar to the previous "continue_from" logic, which was needed to handle the "one consistent offset per dividing plane" explained at

To decide on a noise value for each node, we break the dodecahedral tiling up into these planes. We associate each plane with a randomly chosen noise delta, such that crossing a specific plane in one direction increases or decreases the noise value by a specific amount, and crossing the same plane from the other side has the opposite effect. Once we decide on a noise value for the root node, this definition fully determines the noise value of every other node.
. Effectively, if you have the value at three out of four nodes sharing an edge, the value for the fourth node is completely determined, and this logic figures out what it is.

Copy link
Contributor Author

@DimpyRed DimpyRed Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ralith imagine we have a grid of 4 nodes laid out as follows:

c   | b
-----------
a   | ab

We know a scalar value at a, b, and ab.

We can calculate that crossing the boundary from ab->a increases the value by (a.value - ab.value), and crossing the boundary from ab->b increases the value by (b.value - ab.value).

The way hypermine random-step noise functions work, we declare that the effect on the value of moving from ab->a is the same as that of b->c, in other words a plane is one big boundary that acts the same everywhere on it.

From that we calculate the value of c as (ab.value + (a.value - ab.value) + (b.value - ab.value)), which simplifies to the classic expression (a.value + (b.value - ab.value))

// 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()))
Comment on lines +693 to +706
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: this is kind of hard to visually parse and has serious rightward drift. Lift out some intermediate variables?

.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,
),
}
}
}
Expand Down