Skip to content

Instantly share code, notes, and snippets.

@CoffeeVampir3
Created May 6, 2026 17:02
Show Gist options
  • Select an option

  • Save CoffeeVampir3/8a4788aa14274fbf37073c2528a1adf7 to your computer and use it in GitHub Desktop.

Select an option

Save CoffeeVampir3/8a4788aa14274fbf37073c2528a1adf7 to your computer and use it in GitHub Desktop.
Gumbel-max based spatially covariant feature-gated sampling + correction
use std::simd::{
Select, Simd, StdFloat,
cmp::SimdPartialOrd,
num::{SimdFloat, SimdInt, SimdUint},
};
use bevy::prelude::*;
use crate::{
cell::{CellKind, MaterialGroup, RENDERABLE_CELL_KIND_COUNT},
config::GridConfig,
fast_math::{exp_approx_f32, exp_approx_simd},
grid_math::hash_u32,
};
const GENERATION_TEMPERATURE: f32 = 0.58;
const GROUP_GUMBEL_SCALE: f32 = 0.20;
const GROUP_TEXTURE_WEIGHT: f32 = 1.05;
const MATERIAL_STREAK_WEIGHT: f32 = 1.40;
const FEATURE_MATERIAL_GUMBEL_SCALE: f32 = 0.14;
const TEXTURE_LOG_AMPLITUDE_MIN: f32 = -3.0;
const TEXTURE_LOG_AMPLITUDE_MAX: f32 = 1.85;
const TEXTURE_VALUE_MAX: f32 = 1.35;
const DEPOSIT_FEATURE_SPACING: i32 = 32;
const INTRUSION_FEATURE_SPACING: i32 = 340;
const INTRUSION_RADIUS: f32 = 135.0 / 1.15;
const MATERIAL_COUNT: usize = RENDERABLE_CELL_KIND_COUNT;
const GROUP_COUNT: usize = 13;
const GROUP_SIMD_LANES: usize = 16;
const WORLDGEN_SIMD_LANES: usize = 8;
const SALT_DEPOSIT_FEATURE: u32 = 0xD107_A05E;
const SALT_INTRUSION_FEATURE: u32 = 0xA76C_0B1D;
const SALT_GROUP_GUMBEL: u32 = 0x37A5_C81F;
const SALT_MATERIAL_GUMBEL: u32 = 0x4F8A_71D2;
impl MaterialGroup {
const ALL: [MaterialGroup; GROUP_COUNT] = [
MaterialGroup::Rock,
MaterialGroup::Carbon,
MaterialGroup::BaseMetal,
MaterialGroup::IndustrialMetal,
MaterialGroup::PreciousMetal,
MaterialGroup::RefractoryMetal,
MaterialGroup::RareMetal,
MaterialGroup::RadioactiveMetal,
MaterialGroup::HeavyMetal,
MaterialGroup::Sedimentary,
MaterialGroup::Gem,
MaterialGroup::Mythical,
MaterialGroup::Prize,
];
fn index(self) -> usize {
match self {
MaterialGroup::Rock => 0,
MaterialGroup::Carbon => 1,
MaterialGroup::BaseMetal => 2,
MaterialGroup::IndustrialMetal => 3,
MaterialGroup::PreciousMetal => 4,
MaterialGroup::RefractoryMetal => 5,
MaterialGroup::RareMetal => 6,
MaterialGroup::RadioactiveMetal => 7,
MaterialGroup::HeavyMetal => 8,
MaterialGroup::Sedimentary => 9,
MaterialGroup::Gem => 10,
MaterialGroup::Mythical => 11,
MaterialGroup::Prize => 12,
}
}
}
#[derive(Clone, Copy)]
struct Basis {
depth: f32,
strata_depth: f32,
lo_fbm: f32,
hi_fbm: f32,
ridge: f32,
intrusion: f32,
fracture: f32,
temperature: f32,
limestone: f32,
}
#[derive(Clone, Copy)]
struct GroupTheory {
alpha: f32,
depth_center: f32,
depth_width: f32,
depth_strength: f32,
lo_fbm: f32,
hi_fbm: f32,
ridge: f32,
intrusion: f32,
fracture: f32,
temperature: f32,
limestone: f32,
texture_amplitude: f32,
texture_frequency: f32,
texture_octaves: u32,
texture_ridge_mix: f32,
shared_texture_weight: f32,
}
#[derive(Clone, Copy)]
struct MaterialSpec {
kind: CellKind,
group: MaterialGroup,
alpha: f32,
stratum_offset: f32,
stratum_strength: f32,
}
#[derive(Clone, Copy)]
struct DepositFeature {
id: u32,
center: IVec2,
}
type FeatureMaterialPalette = [usize; GROUP_COUNT];
type GroupSimd = Simd<f32, GROUP_SIMD_LANES>;
type WorldFloatSimd = Simd<f32, WORLDGEN_SIMD_LANES>;
type WorldIntSimd = Simd<i32, WORLDGEN_SIMD_LANES>;
type WorldUintSimd = Simd<u32, WORLDGEN_SIMD_LANES>;
const WORLD_LANE_OFFSETS_I32: [i32; WORLDGEN_SIMD_LANES] = [0, 1, 2, 3, 4, 5, 6, 7];
#[derive(Clone, Copy)]
struct FeatureCache {
palette: FeatureMaterialPalette,
group_gumbel_score: [f32; GROUP_SIMD_LANES],
}
#[cfg(test)]
#[derive(Clone, Copy)]
struct CheapGroupSample {
logit: f32,
texture_amplitude: f32,
}
const GROUP_THEORY: [GroupTheory; GROUP_COUNT] = [
GroupTheory {
alpha: 1.18,
depth_center: 0.0,
depth_width: 3.0,
depth_strength: 0.0,
lo_fbm: -0.18,
hi_fbm: 0.0,
ridge: -0.30,
intrusion: -0.26,
fracture: -0.26,
temperature: 0.0,
limestone: 0.0,
texture_amplitude: 0.0,
texture_frequency: 0.030,
texture_octaves: 2,
texture_ridge_mix: 0.0,
shared_texture_weight: 1.0,
},
GroupTheory {
alpha: 0.10,
depth_center: -0.72,
depth_width: 1.60,
depth_strength: 1.20,
lo_fbm: 0.95,
hi_fbm: 0.15,
ridge: 0.10,
intrusion: -0.25,
fracture: 0.20,
temperature: -0.55,
limestone: 0.50,
texture_amplitude: 0.22,
texture_frequency: 0.026,
texture_octaves: 3,
texture_ridge_mix: 0.12,
shared_texture_weight: 0.55,
},
GroupTheory {
alpha: -1.20,
depth_center: -0.05,
depth_width: 1.50,
depth_strength: 1.05,
lo_fbm: 0.35,
hi_fbm: 0.25,
ridge: 0.85,
intrusion: 0.80,
fracture: 1.05,
temperature: -0.15,
limestone: 0.10,
texture_amplitude: 0.18,
texture_frequency: 0.035,
texture_octaves: 3,
texture_ridge_mix: 0.25,
shared_texture_weight: 0.58,
},
GroupTheory {
alpha: -1.55,
depth_center: 0.18,
depth_width: 1.50,
depth_strength: 0.95,
lo_fbm: 0.10,
hi_fbm: 0.60,
ridge: 0.70,
intrusion: 0.50,
fracture: 0.50,
temperature: 0.05,
limestone: 0.10,
texture_amplitude: 0.16,
texture_frequency: 0.044,
texture_octaves: 3,
texture_ridge_mix: 0.25,
shared_texture_weight: 0.48,
},
GroupTheory {
alpha: -2.35,
depth_center: 0.72,
depth_width: 0.90,
depth_strength: 1.20,
lo_fbm: 0.05,
hi_fbm: 0.40,
ridge: 1.20,
intrusion: 1.55,
fracture: 1.45,
temperature: 0.55,
limestone: 0.0,
texture_amplitude: 0.23,
texture_frequency: 0.048,
texture_octaves: 3,
texture_ridge_mix: 0.40,
shared_texture_weight: 0.70,
},
GroupTheory {
alpha: -1.05,
depth_center: 1.05,
depth_width: 1.30,
depth_strength: 1.25,
lo_fbm: 0.0,
hi_fbm: 0.45,
ridge: 1.00,
intrusion: 1.15,
fracture: 0.80,
temperature: 0.75,
limestone: 0.0,
texture_amplitude: 0.20,
texture_frequency: 0.050,
texture_octaves: 3,
texture_ridge_mix: 0.38,
shared_texture_weight: 0.64,
},
GroupTheory {
alpha: -1.05,
depth_center: 1.20,
depth_width: 1.45,
depth_strength: 1.15,
lo_fbm: 0.25,
hi_fbm: 0.50,
ridge: 0.45,
intrusion: 1.20,
fracture: 0.60,
temperature: 0.45,
limestone: 0.55,
texture_amplitude: 0.20,
texture_frequency: 0.038,
texture_octaves: 3,
texture_ridge_mix: 0.25,
shared_texture_weight: 0.55,
},
GroupTheory {
alpha: -0.95,
depth_center: 1.65,
depth_width: 1.35,
depth_strength: 1.35,
lo_fbm: 0.35,
hi_fbm: 0.45,
ridge: 0.55,
intrusion: 1.10,
fracture: 1.00,
temperature: 0.35,
limestone: 0.25,
texture_amplitude: 0.24,
texture_frequency: 0.034,
texture_octaves: 3,
texture_ridge_mix: 0.28,
shared_texture_weight: 0.50,
},
GroupTheory {
alpha: -2.30,
depth_center: 0.35,
depth_width: 1.10,
depth_strength: 1.00,
lo_fbm: 0.25,
hi_fbm: 0.20,
ridge: 0.45,
intrusion: 0.75,
fracture: 0.90,
temperature: -0.25,
limestone: 0.45,
texture_amplitude: 0.18,
texture_frequency: 0.032,
texture_octaves: 3,
texture_ridge_mix: 0.20,
shared_texture_weight: 0.50,
},
GroupTheory {
alpha: -0.05,
depth_center: -0.10,
depth_width: 1.80,
depth_strength: 1.10,
lo_fbm: 0.55,
hi_fbm: 0.0,
ridge: -0.20,
intrusion: -0.32,
fracture: -0.10,
temperature: -0.30,
limestone: 0.50,
texture_amplitude: 0.18,
texture_frequency: 0.014,
texture_octaves: 3,
texture_ridge_mix: 0.05,
shared_texture_weight: 0.35,
},
GroupTheory {
alpha: -0.55,
depth_center: 0.15,
depth_width: 2.40,
depth_strength: 0.45,
lo_fbm: 0.10,
hi_fbm: 0.40,
ridge: 0.50,
intrusion: 0.40,
fracture: 0.95,
temperature: 0.20,
limestone: 0.10,
texture_amplitude: 0.20,
texture_frequency: 0.040,
texture_octaves: 3,
texture_ridge_mix: 0.30,
shared_texture_weight: 0.55,
},
GroupTheory {
alpha: -1.80,
depth_center: 1.05,
depth_width: 1.35,
depth_strength: 1.10,
lo_fbm: 0.0,
hi_fbm: 0.30,
ridge: 0.80,
intrusion: 1.20,
fracture: 0.95,
temperature: 0.55,
limestone: 0.0,
texture_amplitude: 0.22,
texture_frequency: 0.044,
texture_octaves: 3,
texture_ridge_mix: 0.30,
shared_texture_weight: 0.60,
},
GroupTheory {
alpha: -2.30,
depth_center: 0.55,
depth_width: 0.95,
depth_strength: 1.30,
lo_fbm: -0.20,
hi_fbm: 0.30,
ridge: 0.80,
intrusion: 1.30,
fracture: 1.10,
temperature: 0.40,
limestone: 0.30,
texture_amplitude: 0.0,
texture_frequency: 0.020,
texture_octaves: 1,
texture_ridge_mix: 0.0,
shared_texture_weight: 0.0,
},
];
macro_rules! group_theory_simd_array {
($field:ident, $pad:expr) => {
[
GROUP_THEORY[0].$field,
GROUP_THEORY[1].$field,
GROUP_THEORY[2].$field,
GROUP_THEORY[3].$field,
GROUP_THEORY[4].$field,
GROUP_THEORY[5].$field,
GROUP_THEORY[6].$field,
GROUP_THEORY[7].$field,
GROUP_THEORY[8].$field,
GROUP_THEORY[9].$field,
GROUP_THEORY[10].$field,
GROUP_THEORY[11].$field,
GROUP_THEORY[12].$field,
$pad,
$pad,
$pad,
]
};
}
const GROUP_ALPHA_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(alpha, -1.0e9);
const GROUP_DEPTH_CENTER_SIMD: [f32; GROUP_SIMD_LANES] =
group_theory_simd_array!(depth_center, 0.0);
const GROUP_DEPTH_WIDTH_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(depth_width, 1.0);
const GROUP_DEPTH_STRENGTH_SIMD: [f32; GROUP_SIMD_LANES] =
group_theory_simd_array!(depth_strength, 0.0);
const GROUP_LO_FBM_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(lo_fbm, 0.0);
const GROUP_HI_FBM_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(hi_fbm, 0.0);
const GROUP_RIDGE_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(ridge, 0.0);
const GROUP_INTRUSION_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(intrusion, 0.0);
const GROUP_FRACTURE_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(fracture, 0.0);
const GROUP_TEMPERATURE_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(temperature, 0.0);
const GROUP_LIMESTONE_SIMD: [f32; GROUP_SIMD_LANES] = group_theory_simd_array!(limestone, 0.0);
const GROUP_TEXTURE_AMPLITUDE_SIMD: [f32; GROUP_SIMD_LANES] =
group_theory_simd_array!(texture_amplitude, 0.0);
macro_rules! material_spec {
($kind:expr, $alpha:expr, $stratum_offset:expr, $stratum_strength:expr $(,)?) => {
MaterialSpec {
kind: $kind,
group: $kind.material_group(),
alpha: $alpha,
stratum_offset: $stratum_offset,
stratum_strength: $stratum_strength,
}
};
}
const MATERIAL_SPECS: [MaterialSpec; MATERIAL_COUNT] = [
material_spec!(CellKind::Stone, 2.65, 0.0, 0.0),
material_spec!(CellKind::Coal, 0.65, -0.52, 1.30),
material_spec!(CellKind::Graphite, -0.35, 0.34, 1.10),
material_spec!(CellKind::Diamond, -2.10, 0.86, 1.60),
material_spec!(CellKind::Iron, 0.55, -0.34, 1.10),
material_spec!(CellKind::Copper, 0.20, -0.18, 1.20),
material_spec!(CellKind::Tin, -0.30, -0.05, 1.10),
material_spec!(CellKind::Lead, -0.15, 0.22, 1.20),
material_spec!(CellKind::Zinc, -0.20, 0.28, 1.10),
material_spec!(CellKind::Nickel, -0.55, 0.45, 1.10),
material_spec!(CellKind::Cobalt, -0.75, 0.55, 1.05),
material_spec!(CellKind::Bronze, -1.25, 0.06, 0.85),
material_spec!(CellKind::Aluminum, 0.10, -0.50, 1.10),
material_spec!(CellKind::Titanium, -0.45, 0.40, 1.20),
material_spec!(CellKind::Chromium, -0.40, 0.28, 1.10),
material_spec!(CellKind::Manganese, -0.25, 0.05, 1.10),
material_spec!(CellKind::Magnesium, -0.05, -0.55, 1.00),
material_spec!(CellKind::Silver, -0.05, 0.38, 1.20),
material_spec!(CellKind::Gold, -0.55, 0.52, 1.25),
material_spec!(CellKind::Platinum, -1.25, 0.74, 1.10),
material_spec!(CellKind::Palladium, -1.15, 0.64, 1.10),
material_spec!(CellKind::Tungsten, -0.40, 1.45, 1.20),
material_spec!(CellKind::Molybdenum, -0.60, 1.20, 1.15),
material_spec!(CellKind::Vanadium, -0.35, 0.90, 1.00),
material_spec!(CellKind::Lithium, -0.15, 0.24, 1.20),
material_spec!(CellKind::Beryllium, -0.65, 0.70, 1.10),
material_spec!(CellKind::Niobium, -0.85, 1.10, 1.15),
material_spec!(CellKind::Tantalum, -1.15, 1.45, 1.20),
material_spec!(CellKind::RareEarth, -0.70, 1.25, 1.25),
material_spec!(CellKind::Uranium, -0.45, 1.65, 1.25),
material_spec!(CellKind::Thorium, -0.75, 1.95, 1.25),
material_spec!(CellKind::Mercury, -0.70, 0.18, 1.00),
material_spec!(CellKind::Bismuth, -0.80, 0.32, 1.00),
material_spec!(CellKind::Limestone, 1.20, -0.62, 1.20),
material_spec!(CellKind::Sandstone, 1.00, -0.42, 1.10),
material_spec!(CellKind::Marble, 0.40, -0.32, 1.10),
material_spec!(CellKind::Shale, 0.85, 0.00, 1.00),
material_spec!(CellKind::Slate, 0.60, 0.30, 1.05),
material_spec!(CellKind::Granite, 0.70, 0.55, 1.10),
material_spec!(CellKind::Basalt, 0.65, 0.95, 1.15),
material_spec!(CellKind::Quartz, 0.10, -0.30, 0.70),
material_spec!(CellKind::Amethyst, 0.05, -0.40, 1.20),
material_spec!(CellKind::Opal, -0.05, 0.00, 1.15),
material_spec!(CellKind::Topaz, 0.05, 0.10, 1.20),
material_spec!(CellKind::Jade, -0.05, 0.20, 1.15),
material_spec!(CellKind::Emerald, -0.30, 0.50, 1.30),
material_spec!(CellKind::Sapphire, -0.45, 0.70, 1.30),
material_spec!(CellKind::Ruby, -0.50, 0.85, 1.30),
material_spec!(CellKind::Aetherite, 0.00, 0.20, 1.10),
material_spec!(CellKind::Orichalcum, -0.10, 1.35, 1.25),
material_spec!(CellKind::Mythril, -0.40, 0.65, 1.15),
material_spec!(CellKind::Soulstone, -0.70, 0.80, 1.15),
material_spec!(CellKind::Starsteel, -0.85, 1.00, 1.20),
material_spec!(CellKind::Adamantite, -0.30, 1.75, 1.35),
material_spec!(CellKind::Voidstone, -1.50, 1.60, 1.20),
material_spec!(CellKind::Impossibilium, -0.75, 2.15, 1.55),
material_spec!(CellKind::Snorgite, 0.05, -0.35, 1.20),
material_spec!(CellKind::Bonkrock, -0.05, -0.05, 1.15),
material_spec!(CellKind::SuspiciouslySlimeyRock, -0.10, -0.15, 1.10),
material_spec!(CellKind::AncientBronze, 0.20, 0.20, 1.05),
material_spec!(CellKind::MeteoricSilver, -0.10, 0.40, 1.10),
material_spec!(CellKind::SolidGold, -0.40, 0.55, 1.15),
material_spec!(CellKind::GildedDiamond, -0.90, 0.85, 1.20),
];
fn group_layer_width(group: MaterialGroup) -> f32 {
match group {
MaterialGroup::Rock => 3.0,
MaterialGroup::Carbon => 0.46,
MaterialGroup::BaseMetal => 0.38,
MaterialGroup::IndustrialMetal => 0.42,
MaterialGroup::PreciousMetal => 0.34,
MaterialGroup::RefractoryMetal => 0.50,
MaterialGroup::RareMetal => 0.52,
MaterialGroup::RadioactiveMetal => 0.58,
MaterialGroup::HeavyMetal => 0.38,
MaterialGroup::Sedimentary => 0.55,
MaterialGroup::Gem => 0.48,
MaterialGroup::Mythical => 0.42,
MaterialGroup::Prize => 0.36,
}
}
fn group_streak_frequency(group: MaterialGroup) -> f32 {
match group {
MaterialGroup::Rock => 0.010,
MaterialGroup::Carbon => 0.026,
MaterialGroup::BaseMetal => 0.030,
MaterialGroup::IndustrialMetal => 0.028,
MaterialGroup::PreciousMetal => 0.038,
MaterialGroup::RefractoryMetal => 0.034,
MaterialGroup::RareMetal => 0.030,
MaterialGroup::RadioactiveMetal => 0.026,
MaterialGroup::HeavyMetal => 0.030,
MaterialGroup::Sedimentary => 0.012,
MaterialGroup::Gem => 0.034,
MaterialGroup::Mythical => 0.032,
MaterialGroup::Prize => 0.020,
}
}
fn group_streak_offset(group: MaterialGroup) -> f32 {
match group {
MaterialGroup::Rock => 0.0,
MaterialGroup::Carbon => 0.05,
MaterialGroup::BaseMetal => 0.35,
MaterialGroup::IndustrialMetal => -0.25,
MaterialGroup::PreciousMetal => 0.55,
MaterialGroup::RefractoryMetal => -0.50,
MaterialGroup::RareMetal => 0.20,
MaterialGroup::RadioactiveMetal => -0.65,
MaterialGroup::HeavyMetal => 0.10,
MaterialGroup::Sedimentary => 0.0,
MaterialGroup::Gem => 0.30,
MaterialGroup::Mythical => -0.40,
MaterialGroup::Prize => 0.15,
}
}
fn group_streak_stretch(group: MaterialGroup) -> f32 {
match group {
MaterialGroup::Rock => 1.0,
MaterialGroup::Carbon => 4.6,
MaterialGroup::BaseMetal => 5.8,
MaterialGroup::IndustrialMetal => 4.8,
MaterialGroup::PreciousMetal => 6.8,
MaterialGroup::RefractoryMetal => 6.0,
MaterialGroup::RareMetal => 5.2,
MaterialGroup::RadioactiveMetal => 5.0,
MaterialGroup::HeavyMetal => 4.9,
MaterialGroup::Sedimentary => 7.5,
MaterialGroup::Gem => 5.0,
MaterialGroup::Mythical => 5.6,
MaterialGroup::Prize => 3.0,
}
}
pub(crate) fn build_generated_cells(config: &GridConfig, chunk: IVec2) -> Vec<CellKind> {
let mut cells = Vec::with_capacity(config.tiles_per_chunk());
let seed = config.world_seed;
let chunk_features = precompute_chunk_features(seed, chunk, config.chunk_side);
let mut feature_cache = vec![None; chunk_features.len()];
for y in 0..config.chunk_side {
let mut x = 0;
while x + WORLDGEN_SIMD_LANES as i32 <= config.chunk_side {
let cell_start = global_cell_coord(config, chunk, UVec2::new(x as u32, y as u32));
let cell_x = row_cell_x(cell_start);
let cell_y = row_cell_y(cell_start);
let intrusions = intrusion_proximity_simd(seed, cell_x, cell_y);
let basis_row =
BasisRow::from_simd(basis_fields_simd(seed, cell_x, cell_y, intrusions));
let feature_indices = deposit_feature_indices_row(&chunk_features, cell_x, cell_y);
for (lane, feature_index) in feature_indices.into_iter().enumerate() {
let cell = cell_start + IVec2::new(lane as i32, 0);
let basis = basis_row.lane(lane);
let cache =
cached_feature(seed, &chunk_features, &mut feature_cache, feature_index);
cells.push(sample_cell(seed, cell, basis, &cache));
}
x += WORLDGEN_SIMD_LANES as i32;
}
while x < config.chunk_side {
let cell = global_cell_coord(config, chunk, UVec2::new(x as u32, y as u32));
let basis = basis_fields(seed, cell);
let feature_index = deposit_feature_index_from(&chunk_features, cell);
let cache = cached_feature(seed, &chunk_features, &mut feature_cache, feature_index);
cells.push(sample_cell(seed, cell, basis, &cache));
x += 1;
}
}
debug_assert_eq!(cells.len(), config.tiles_per_chunk());
cells
}
fn global_cell_coord(config: &GridConfig, chunk: IVec2, local: UVec2) -> IVec2 {
chunk * config.chunk_side + local.as_ivec2()
}
fn row_cell_x(cell_start: IVec2) -> WorldIntSimd {
Simd::splat(cell_start.x) + Simd::from_array(WORLD_LANE_OFFSETS_I32)
}
fn row_cell_y(cell_start: IVec2) -> WorldIntSimd {
Simd::splat(cell_start.y)
}
fn cached_feature(
seed: u32,
features: &[PrecomputedFeature],
feature_cache: &mut [Option<FeatureCache>],
feature_index: usize,
) -> FeatureCache {
if let Some(cache) = feature_cache[feature_index] {
return cache;
}
let feature = features[feature_index].deposit_feature();
let cache = build_feature_cache(seed, feature);
feature_cache[feature_index] = Some(cache);
cache
}
fn sample_cell(seed: u32, cell: IVec2, basis: Basis, cache: &FeatureCache) -> CellKind {
let (logit_simd, texture_amplitude_simd) = cheap_group_sample_simd(basis);
let score_no_tex_simd = logit_simd * Simd::splat(1.0 / GENERATION_TEMPERATURE)
+ Simd::from_array(cache.group_gumbel_score);
let score_no_tex = group_prefix(score_no_tex_simd);
let texture_amplitude = group_prefix(texture_amplitude_simd);
let mut best_idx: usize = 0;
let mut best_score = f32::NEG_INFINITY;
for i in 0..GROUP_COUNT {
let s = score_no_tex[i];
if texture_amplitude[i] == 0.0 && s > best_score {
best_score = s;
best_idx = i;
}
}
for i in 0..GROUP_COUNT {
let amp = texture_amplitude[i];
if amp == 0.0 {
continue;
}
let max_bonus = amp * TEXTURE_VALUE_MAX * GROUP_TEXTURE_WEIGHT;
if score_no_tex[i] + max_bonus <= best_score {
continue;
}
let group = MaterialGroup::ALL[i];
let theory = GROUP_THEORY[i];
let texture = group_texture_field(seed, cell, group, theory);
let actual = score_no_tex[i] + amp * texture * GROUP_TEXTURE_WEIGHT;
if actual > best_score {
best_score = actual;
best_idx = i;
}
}
MATERIAL_SPECS[cache.palette[best_idx]].kind
}
fn cheap_group_sample_simd(basis: Basis) -> (GroupSimd, GroupSimd) {
let depth_delta = Simd::splat(basis.depth) - Simd::from_array(GROUP_DEPTH_CENTER_SIMD);
let t = (Simd::splat(1.0) - depth_delta.abs() / Simd::from_array(GROUP_DEPTH_WIDTH_SIMD))
.simd_max(Simd::splat(0.0))
.simd_min(Simd::splat(1.0));
let depth = t * t * (Simd::splat(3.0) - t * Simd::splat(2.0));
let depth_strength = Simd::from_array(GROUP_DEPTH_STRENGTH_SIMD);
let feature_logit = Simd::from_array(GROUP_LO_FBM_SIMD) * Simd::splat(basis.lo_fbm)
+ Simd::from_array(GROUP_HI_FBM_SIMD) * Simd::splat(basis.hi_fbm)
+ Simd::from_array(GROUP_RIDGE_SIMD) * Simd::splat(basis.ridge)
+ Simd::from_array(GROUP_INTRUSION_SIMD) * Simd::splat(basis.intrusion)
+ Simd::from_array(GROUP_FRACTURE_SIMD) * Simd::splat(basis.fracture)
+ Simd::from_array(GROUP_TEMPERATURE_SIMD) * Simd::splat(basis.temperature)
+ Simd::from_array(GROUP_LIMESTONE_SIMD) * Simd::splat(basis.limestone);
let structural_logit = depth_strength * depth + feature_logit;
let logit = Simd::from_array(GROUP_ALPHA_SIMD) + structural_logit;
let texture_log_amplitude = structural_logit
.simd_max(Simd::splat(TEXTURE_LOG_AMPLITUDE_MIN))
.simd_min(Simd::splat(TEXTURE_LOG_AMPLITUDE_MAX));
let texture_amplitude =
Simd::from_array(GROUP_TEXTURE_AMPLITUDE_SIMD) * exp_approx_simd(texture_log_amplitude);
(logit, texture_amplitude)
}
fn group_prefix(values: GroupSimd) -> [f32; GROUP_COUNT] {
let values = values.to_array();
std::array::from_fn(|index| values[index])
}
#[cfg(test)]
fn cheap_group_samples(basis: Basis) -> [CheapGroupSample; GROUP_COUNT] {
let (logit, texture_amplitude) = cheap_group_sample_simd(basis);
let logit = group_prefix(logit);
let texture_amplitude = group_prefix(texture_amplitude);
std::array::from_fn(|group_index| CheapGroupSample {
logit: logit[group_index],
texture_amplitude: texture_amplitude[group_index],
})
}
fn build_feature_cache(seed: u32, feature: DepositFeature) -> FeatureCache {
let palette = feature_material_palette(seed, feature);
let group_gumbel_score = std::array::from_fn(|i| {
if i < GROUP_COUNT {
gumbel(seed ^ SALT_GROUP_GUMBEL, feature.id, i as u32) * GROUP_GUMBEL_SCALE
} else {
0.0
}
});
FeatureCache {
palette,
group_gumbel_score,
}
}
#[cfg(test)]
#[derive(Clone, Copy)]
struct GroupSample {
logit: f32,
texture_amplitude: f32,
texture_value: f32,
}
#[cfg(test)]
fn group_samples(seed: u32, cell: IVec2, basis: Basis) -> [GroupSample; GROUP_COUNT] {
let cheap = cheap_group_samples(basis);
std::array::from_fn(|i| {
let theory = GROUP_THEORY[i];
let group = MaterialGroup::ALL[i];
let texture_value = group_texture_field(seed, cell, group, theory);
GroupSample {
logit: cheap[i].logit,
texture_amplitude: cheap[i].texture_amplitude,
texture_value,
}
})
}
fn feature_material_palette(seed: u32, feature: DepositFeature) -> FeatureMaterialPalette {
let basis = basis_fields(seed, feature.center);
std::array::from_fn(|group_index| {
select_feature_material(seed, feature, basis, MaterialGroup::ALL[group_index])
})
}
fn select_feature_material(
seed: u32,
feature: DepositFeature,
basis: Basis,
group: MaterialGroup,
) -> usize {
let mut best_index = 0;
let mut best_score = f32::NEG_INFINITY;
for (material_index, spec) in MATERIAL_SPECS.iter().copied().enumerate() {
if spec.group != group {
continue;
}
let score = material_identity_logit(spec, basis)
+ material_streak_field(seed, feature.center, material_index, spec.group)
* MATERIAL_STREAK_WEIGHT
+ gumbel(
seed ^ SALT_MATERIAL_GUMBEL,
feature.id,
material_index as u32,
) * FEATURE_MATERIAL_GUMBEL_SCALE;
if score > best_score {
best_score = score;
best_index = material_index;
}
}
best_index
}
fn material_identity_logit(spec: MaterialSpec, basis: Basis) -> f32 {
let layer_width = group_layer_width(spec.group);
let t = (1.0 - (basis.strata_depth - spec.stratum_offset).abs() / layer_width).clamp(0.0, 1.0);
let layer_affinity = t * t * (3.0 - 2.0 * t);
spec.alpha + spec.stratum_strength * layer_affinity
}
fn material_streak_field(
seed: u32,
cell: IVec2,
material_index: usize,
group: MaterialGroup,
) -> f32 {
if group == MaterialGroup::Rock {
return 0.0;
}
let salt = (material_index as u32).wrapping_mul(0xA24B_AED5);
let position = cell.as_vec2();
let frequency = group_streak_frequency(group);
let streak = streak_ridge_field(seed ^ 0x49E3_6F4B, position, group, salt, frequency, 3);
let broad = fbm(seed ^ 0xC4CE_B9FE ^ salt, position, frequency * 0.22, 2);
(streak * 0.82 + broad * 0.18).clamp(-1.25, 1.25)
}
fn group_texture_field(seed: u32, cell: IVec2, group: MaterialGroup, theory: GroupTheory) -> f32 {
if theory.texture_amplitude == 0.0 {
return 0.0;
}
let position = cell.as_vec2();
let salt = (group.index() as u32).wrapping_mul(0x8DA6_B343);
let broad = fbm(
seed ^ 0x716C_7A1E ^ salt,
position,
theory.texture_frequency,
theory.texture_octaves,
);
let ridged = ridged_fbm(
seed ^ 0x9E64_DD3A ^ salt,
position,
theory.texture_frequency * 0.82,
2,
);
let streak = streak_ridge_field(
seed ^ 0x50C3_8B49,
position,
group,
salt,
theory.texture_frequency,
theory.texture_octaves,
);
let streak_weight =
(theory.shared_texture_weight + theory.texture_ridge_mix * 0.4).clamp(0.35, 0.90);
let texture = broad * (1.0 - streak_weight)
+ streak * streak_weight
+ ridged * theory.texture_ridge_mix * 0.20;
texture.clamp(-1.35, 1.35)
}
fn streak_ridge_field(
seed: u32,
position: Vec2,
group: MaterialGroup,
salt: u32,
frequency: f32,
octaves: u32,
) -> f32 {
let angle = local_streak_angle(seed, position) + group_streak_offset(group);
let (sin, cos) = angle.sin_cos();
let along = Vec2::new(cos, sin);
let across = Vec2::new(-sin, cos);
let stretch = group_streak_stretch(group);
let streak_position = Vec2::new(
position.dot(along) * frequency / stretch,
position.dot(across) * frequency * stretch,
);
let ridge = ridged_fbm(seed ^ salt ^ 0xB8F4_82AD, streak_position, 1.0, octaves);
let warp = fbm(seed ^ salt ^ 0x4AF2_C91D, position, frequency * 0.18, 2);
(ridge + warp * 0.18).clamp(-1.25, 1.25)
}
fn local_streak_angle(seed: u32, position: Vec2) -> f32 {
let n = value_noise(seed ^ 0xA5C3_91F2, position * 0.0035);
n * std::f32::consts::PI
}
fn basis_fields(seed: u32, cell: IVec2) -> Basis {
basis_fields_with_intrusion(seed, cell, intrusion_proximity(seed, cell))
}
fn basis_fields_with_intrusion(seed: u32, cell: IVec2, intrusion: f32) -> Basis {
let depth = (-cell.y as f32 / 360.0).clamp(-2.4, 2.6);
let lo_fbm = fbm(seed ^ 0x11F0_2A7D, cell.as_vec2(), 0.008, 4);
let hi_fbm = fbm(seed ^ 0x50DA_61A5, cell.as_vec2(), 0.060, 3);
let ridge = ridged_fbm(seed ^ 0xB141_31D0, cell.as_vec2(), 0.025, 3);
let fracture = fracture_proximity(seed ^ 0x8C6D_C07D, cell.as_vec2());
let strata_depth = depth + fbm(seed ^ 0x6C21_7341, cell.as_vec2(), 0.004, 3) * 0.38;
let temperature = (intrusion * 2.0 + lo_fbm * 0.25 - 0.45).clamp(-1.5, 2.4);
let limestone =
(fbm(seed ^ 0x1E57_0A11, cell.as_vec2(), 0.014, 3) + depth * 0.04 > 0.35) as u8 as f32;
Basis {
depth,
strata_depth,
lo_fbm,
hi_fbm,
ridge,
intrusion,
fracture,
temperature,
limestone,
}
}
#[derive(Clone, Copy)]
struct BasisSimd {
depth: WorldFloatSimd,
strata_depth: WorldFloatSimd,
lo_fbm: WorldFloatSimd,
hi_fbm: WorldFloatSimd,
ridge: WorldFloatSimd,
intrusion: WorldFloatSimd,
fracture: WorldFloatSimd,
temperature: WorldFloatSimd,
limestone: WorldFloatSimd,
}
struct BasisRow {
depth: [f32; WORLDGEN_SIMD_LANES],
strata_depth: [f32; WORLDGEN_SIMD_LANES],
lo_fbm: [f32; WORLDGEN_SIMD_LANES],
hi_fbm: [f32; WORLDGEN_SIMD_LANES],
ridge: [f32; WORLDGEN_SIMD_LANES],
intrusion: [f32; WORLDGEN_SIMD_LANES],
fracture: [f32; WORLDGEN_SIMD_LANES],
temperature: [f32; WORLDGEN_SIMD_LANES],
limestone: [f32; WORLDGEN_SIMD_LANES],
}
impl BasisRow {
fn from_simd(basis: BasisSimd) -> Self {
Self {
depth: basis.depth.to_array(),
strata_depth: basis.strata_depth.to_array(),
lo_fbm: basis.lo_fbm.to_array(),
hi_fbm: basis.hi_fbm.to_array(),
ridge: basis.ridge.to_array(),
intrusion: basis.intrusion.to_array(),
fracture: basis.fracture.to_array(),
temperature: basis.temperature.to_array(),
limestone: basis.limestone.to_array(),
}
}
fn lane(&self, lane: usize) -> Basis {
Basis {
depth: self.depth[lane],
strata_depth: self.strata_depth[lane],
lo_fbm: self.lo_fbm[lane],
hi_fbm: self.hi_fbm[lane],
ridge: self.ridge[lane],
intrusion: self.intrusion[lane],
fracture: self.fracture[lane],
temperature: self.temperature[lane],
limestone: self.limestone[lane],
}
}
}
fn basis_fields_simd(
seed: u32,
cell_x: WorldIntSimd,
cell_y: WorldIntSimd,
intrusion: WorldFloatSimd,
) -> BasisSimd {
let x = cell_x.cast::<f32>();
let y = cell_y.cast::<f32>();
let depth = (-y / Simd::splat(360.0))
.simd_max(Simd::splat(-2.4))
.simd_min(Simd::splat(2.6));
let lo_fbm = fbm_simd(seed ^ 0x11F0_2A7D, x, y, 0.008, 4);
let hi_fbm = fbm_simd(seed ^ 0x50DA_61A5, x, y, 0.060, 3);
let ridge = ridged_fbm_simd(seed ^ 0xB141_31D0, x, y, 0.025, 3);
let fracture = fracture_proximity_simd(seed ^ 0x8C6D_C07D, x, y);
let strata_depth = depth + fbm_simd(seed ^ 0x6C21_7341, x, y, 0.004, 3) * Simd::splat(0.38);
let temperature = (intrusion * Simd::splat(2.0) + lo_fbm * Simd::splat(0.25)
- Simd::splat(0.45))
.simd_max(Simd::splat(-1.5))
.simd_min(Simd::splat(2.4));
let limestone = (fbm_simd(seed ^ 0x1E57_0A11, x, y, 0.014, 3) + depth * Simd::splat(0.04))
.simd_gt(Simd::splat(0.35))
.select(Simd::splat(1.0), Simd::splat(0.0));
BasisSimd {
depth,
strata_depth,
lo_fbm,
hi_fbm,
ridge,
intrusion,
fracture,
temperature,
limestone,
}
}
fn fracture_proximity(seed: u32, position: Vec2) -> f32 {
let broad = fbm(seed, position, 0.018, 3).abs();
let fine = fbm(seed ^ 0x5337_EA1D, position, 0.045, 2).abs();
let broad_band = (1.0 - broad / 0.17).clamp(0.0, 1.0);
let fine_band = (1.0 - fine / 0.11).clamp(0.0, 1.0);
(broad_band * 0.75 + fine_band * 0.25).clamp(0.0, 1.0)
}
fn fracture_proximity_simd(seed: u32, x: WorldFloatSimd, y: WorldFloatSimd) -> WorldFloatSimd {
let broad = fbm_simd(seed, x, y, 0.018, 3).abs();
let fine = fbm_simd(seed ^ 0x5337_EA1D, x, y, 0.045, 2).abs();
let broad_band = (Simd::splat(1.0) - broad / Simd::splat(0.17))
.simd_max(Simd::splat(0.0))
.simd_min(Simd::splat(1.0));
let fine_band = (Simd::splat(1.0) - fine / Simd::splat(0.11))
.simd_max(Simd::splat(0.0))
.simd_min(Simd::splat(1.0));
(broad_band * Simd::splat(0.75) + fine_band * Simd::splat(0.25))
.simd_max(Simd::splat(0.0))
.simd_min(Simd::splat(1.0))
}
fn intrusion_proximity(seed: u32, cell: IVec2) -> f32 {
let nearest_squared = nearest_feature_distance_squared(
seed ^ SALT_INTRUSION_FEATURE,
cell,
INTRUSION_FEATURE_SPACING,
);
exp_approx_f32(-nearest_squared.sqrt() / INTRUSION_RADIUS)
}
fn intrusion_proximity_simd(
seed: u32,
cell_x: WorldIntSimd,
cell_y: WorldIntSimd,
) -> WorldFloatSimd {
let seed = seed ^ SALT_INTRUSION_FEATURE;
let nearest_squared =
nearest_feature_distance_squared_simd(seed, cell_x, cell_y, INTRUSION_FEATURE_SPACING);
let scaled_distance = -nearest_squared.sqrt() / Simd::splat(INTRUSION_RADIUS);
exp_approx_simd(scaled_distance)
}
fn nearest_feature_distance_squared(seed: u32, cell: IVec2, feature_spacing: i32) -> f32 {
let grid = IVec2::new(
cell.x.div_euclid(feature_spacing),
cell.y.div_euclid(feature_spacing),
);
let mut nearest_squared = f32::INFINITY;
for oy in -1..=1 {
for ox in -1..=1 {
let feature_cell = grid + IVec2::new(ox, oy);
let center = jittered_feature_center(seed, feature_cell, feature_spacing);
nearest_squared = nearest_squared.min(cell.as_vec2().distance_squared(center));
}
}
nearest_squared
}
fn nearest_feature_distance_squared_simd(
seed: u32,
cell_x: WorldIntSimd,
cell_y: WorldIntSimd,
feature_spacing: i32,
) -> WorldFloatSimd {
let grid_x = div_euclid_simd(cell_x, feature_spacing);
let grid_y = div_euclid_simd(cell_y, feature_spacing);
let pos_x = cell_x.cast::<f32>();
let pos_y = cell_y.cast::<f32>();
let mut nearest_squared = Simd::splat(f32::INFINITY);
for oy in -1..=1 {
for ox in -1..=1 {
let feature_cell_x = grid_x + Simd::splat(ox);
let feature_cell_y = grid_y + Simd::splat(oy);
let (center_x, center_y) =
jittered_feature_center_simd(seed, feature_cell_x, feature_cell_y, feature_spacing);
let dx = pos_x - center_x;
let dy = pos_y - center_y;
nearest_squared = nearest_squared.simd_min(dx * dx + dy * dy);
}
}
nearest_squared
}
fn jittered_feature_center(seed: u32, feature_cell: IVec2, spacing: i32) -> Vec2 {
let base = feature_cell * spacing;
let x_hash = hash2(seed ^ 0xC2B2_AE35, feature_cell, 0);
let y_hash = hash2(seed ^ 0x27D4_EB2F, feature_cell, 1);
let jitter =
Vec2::new(unit_hash(x_hash) - 0.5, unit_hash(y_hash) - 0.5) * spacing as f32 * 0.72;
base.as_vec2() + Vec2::splat(spacing as f32 * 0.5) + jitter
}
fn jittered_feature_center_simd(
seed: u32,
feature_cell_x: WorldIntSimd,
feature_cell_y: WorldIntSimd,
spacing: i32,
) -> (WorldFloatSimd, WorldFloatSimd) {
let spacing_f = Simd::splat(spacing as f32);
let base_x = (feature_cell_x * Simd::splat(spacing)).cast::<f32>();
let base_y = (feature_cell_y * Simd::splat(spacing)).cast::<f32>();
let x_hash = hash2_simd(seed ^ 0xC2B2_AE35, feature_cell_x, feature_cell_y, 0);
let y_hash = hash2_simd(seed ^ 0x27D4_EB2F, feature_cell_x, feature_cell_y, 1);
let jitter_x = (unit_hash_simd(x_hash) - Simd::splat(0.5)) * spacing_f * Simd::splat(0.72);
let jitter_y = (unit_hash_simd(y_hash) - Simd::splat(0.5)) * spacing_f * Simd::splat(0.72);
let half_spacing = spacing_f * Simd::splat(0.5);
(
base_x + half_spacing + jitter_x,
base_y + half_spacing + jitter_y,
)
}
#[derive(Clone, Copy)]
struct PrecomputedFeature {
id: u32,
center: Vec2,
}
impl PrecomputedFeature {
fn deposit_feature(self) -> DepositFeature {
DepositFeature {
id: self.id,
center: self.center.round().as_ivec2(),
}
}
}
fn precompute_chunk_features(seed: u32, chunk: IVec2, chunk_side: i32) -> Vec<PrecomputedFeature> {
let salted_seed = seed ^ SALT_DEPOSIT_FEATURE;
let chunk_min = chunk * chunk_side;
let chunk_max = chunk_min + IVec2::splat(chunk_side - 1);
let grid_min = IVec2::new(
chunk_min.x.div_euclid(DEPOSIT_FEATURE_SPACING) - 1,
chunk_min.y.div_euclid(DEPOSIT_FEATURE_SPACING) - 1,
);
let grid_max = IVec2::new(
chunk_max.x.div_euclid(DEPOSIT_FEATURE_SPACING) + 1,
chunk_max.y.div_euclid(DEPOSIT_FEATURE_SPACING) + 1,
);
let mut features = Vec::with_capacity(16);
for gy in grid_min.y..=grid_max.y {
for gx in grid_min.x..=grid_max.x {
let feature_cell = IVec2::new(gx, gy);
let center =
jittered_feature_center(salted_seed, feature_cell, DEPOSIT_FEATURE_SPACING);
let id = hash2(salted_seed, feature_cell, 0);
features.push(PrecomputedFeature { id, center });
}
}
features
}
fn deposit_feature_index_from(features: &[PrecomputedFeature], cell: IVec2) -> usize {
let pos = cell.as_vec2();
let mut nearest_dsq = f32::INFINITY;
let mut nearest_index = 0;
for (index, f) in features.iter().enumerate() {
let dsq = pos.distance_squared(f.center);
if dsq < nearest_dsq {
nearest_dsq = dsq;
nearest_index = index;
}
}
nearest_index
}
fn deposit_feature_indices_row(
features: &[PrecomputedFeature],
cell_x: WorldIntSimd,
cell_y: WorldIntSimd,
) -> [usize; WORLDGEN_SIMD_LANES] {
let pos_x = cell_x.cast::<f32>();
let pos_y = cell_y.cast::<f32>();
let mut nearest_dsq = Simd::splat(f32::INFINITY);
let mut nearest_index = Simd::<u32, WORLDGEN_SIMD_LANES>::splat(0);
for (index, feature) in features.iter().enumerate() {
let dx = pos_x - Simd::splat(feature.center.x);
let dy = pos_y - Simd::splat(feature.center.y);
let dsq = dx * dx + dy * dy;
let is_nearest = dsq.simd_lt(nearest_dsq);
nearest_dsq = is_nearest.select(dsq, nearest_dsq);
nearest_index = is_nearest.select(Simd::splat(index as u32), nearest_index);
}
let nearest_index = nearest_index.to_array();
std::array::from_fn(|lane| nearest_index[lane] as usize)
}
fn fbm(seed: u32, position: Vec2, frequency: f32, octaves: u32) -> f32 {
let mut value = 0.0;
let mut amplitude = 0.5;
let mut total_amplitude = 0.0;
let mut octave_frequency = frequency;
for octave in 0..octaves {
value += value_noise(
seed ^ octave.wrapping_mul(0x9E37_79B9),
position * octave_frequency,
) * amplitude;
total_amplitude += amplitude;
amplitude *= 0.5;
octave_frequency *= 2.0;
}
value / total_amplitude
}
fn fbm_simd(
seed: u32,
position_x: WorldFloatSimd,
position_y: WorldFloatSimd,
frequency: f32,
octaves: u32,
) -> WorldFloatSimd {
let mut value = Simd::splat(0.0);
let mut amplitude = 0.5;
let mut total_amplitude = 0.0;
let mut octave_frequency = frequency;
for octave in 0..octaves {
value += value_noise_simd(
seed ^ octave.wrapping_mul(0x9E37_79B9),
position_x * Simd::splat(octave_frequency),
position_y * Simd::splat(octave_frequency),
) * Simd::splat(amplitude);
total_amplitude += amplitude;
amplitude *= 0.5;
octave_frequency *= 2.0;
}
value / Simd::splat(total_amplitude)
}
fn ridged_fbm(seed: u32, position: Vec2, frequency: f32, octaves: u32) -> f32 {
let value = fbm(seed, position, frequency, octaves).abs();
(1.0 - value / 0.22).clamp(0.0, 1.0).mul_add(2.0, -1.0)
}
fn ridged_fbm_simd(
seed: u32,
position_x: WorldFloatSimd,
position_y: WorldFloatSimd,
frequency: f32,
octaves: u32,
) -> WorldFloatSimd {
let value = fbm_simd(seed, position_x, position_y, frequency, octaves).abs();
let ridge = (Simd::splat(1.0) - value / Simd::splat(0.22))
.simd_max(Simd::splat(0.0))
.simd_min(Simd::splat(1.0));
ridge * Simd::splat(2.0) - Simd::splat(1.0)
}
fn value_noise(seed: u32, position: Vec2) -> f32 {
let cell = position.floor().as_ivec2();
let local = position - cell.as_vec2();
let smooth = local * local * (Vec2::splat(3.0) - local * 2.0);
let a = lattice_noise(seed, cell);
let b = lattice_noise(seed, cell + IVec2::X);
let c = lattice_noise(seed, cell + IVec2::Y);
let d = lattice_noise(seed, cell + IVec2::ONE);
let x0 = a.lerp(b, smooth.x);
let x1 = c.lerp(d, smooth.x);
x0.lerp(x1, smooth.y)
}
fn value_noise_simd(
seed: u32,
position_x: WorldFloatSimd,
position_y: WorldFloatSimd,
) -> WorldFloatSimd {
let cell_x = position_x.floor().cast::<i32>();
let cell_y = position_y.floor().cast::<i32>();
let local_x = position_x - cell_x.cast::<f32>();
let local_y = position_y - cell_y.cast::<f32>();
let smooth_x = local_x * local_x * (Simd::splat(3.0) - local_x * Simd::splat(2.0));
let smooth_y = local_y * local_y * (Simd::splat(3.0) - local_y * Simd::splat(2.0));
let a = lattice_noise_simd(seed, cell_x, cell_y);
let b = lattice_noise_simd(seed, cell_x + Simd::splat(1), cell_y);
let c = lattice_noise_simd(seed, cell_x, cell_y + Simd::splat(1));
let d = lattice_noise_simd(seed, cell_x + Simd::splat(1), cell_y + Simd::splat(1));
let x0 = a + (b - a) * smooth_x;
let x1 = c + (d - c) * smooth_x;
x0 + (x1 - x0) * smooth_y
}
fn lattice_noise(seed: u32, cell: IVec2) -> f32 {
unit_hash(hash2(seed, cell, 0)).mul_add(2.0, -1.0)
}
fn lattice_noise_simd(seed: u32, cell_x: WorldIntSimd, cell_y: WorldIntSimd) -> WorldFloatSimd {
unit_hash_simd(hash2_simd(seed, cell_x, cell_y, 0)) * Simd::splat(2.0) - Simd::splat(1.0)
}
fn gumbel(seed: u32, key: u32, class: u32) -> f32 {
let mixed = seed ^ key.wrapping_mul(0x9E37_79B9) ^ class.wrapping_mul(0xC2B2_AE35);
let u = unit_hash(hash_u32(mixed)).clamp(1.0e-6, 1.0 - 1.0e-6);
-(-u.ln()).ln()
}
fn unit_hash(hash: u32) -> f32 {
(hash as f32 + 0.5) / (u32::MAX as f32 + 1.0)
}
fn unit_hash_simd(hash: WorldUintSimd) -> WorldFloatSimd {
(hash.cast::<f32>() + Simd::splat(0.5)) / Simd::splat(u32::MAX as f32 + 1.0)
}
fn hash2(seed: u32, cell: IVec2, salt: u32) -> u32 {
hash_u32(
seed ^ (cell.x as u32).wrapping_mul(0x9E37_79B9)
^ (cell.y as u32).wrapping_mul(0x85EB_CA6B)
^ salt.wrapping_mul(0xC2B2_AE35),
)
}
fn hash2_simd(seed: u32, cell_x: WorldIntSimd, cell_y: WorldIntSimd, salt: u32) -> WorldUintSimd {
hash_u32_simd(
Simd::splat(seed)
^ (cell_x.cast::<u32>() * Simd::splat(0x9E37_79B9))
^ (cell_y.cast::<u32>() * Simd::splat(0x85EB_CA6B))
^ Simd::splat(salt.wrapping_mul(0xC2B2_AE35)),
)
}
fn hash_u32_simd(mut value: WorldUintSimd) -> WorldUintSimd {
value ^= value >> Simd::splat(16);
value *= Simd::splat(0x7FEB_352D);
value ^= value >> Simd::splat(15);
value *= Simd::splat(0x846C_A68B);
value ^ (value >> Simd::splat(16))
}
fn div_euclid_simd(value: WorldIntSimd, rhs: i32) -> WorldIntSimd {
Simd::from_array(value.to_array().map(|value| value.div_euclid(rhs)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{cell::RENDERABLE_CELL_KINDS, grid_math::tile_index};
#[test]
fn material_specs_match_renderable_cell_order() {
for (index, kind) in RENDERABLE_CELL_KINDS.into_iter().enumerate() {
assert_eq!(MATERIAL_SPECS[index].kind, kind);
assert_eq!(kind.tileset_index(), Some(index as u16));
}
}
#[test]
fn new_materials_have_their_intended_strata() {
for kind in [
CellKind::Orichalcum,
CellKind::Adamantite,
CellKind::Impossibilium,
] {
let target = material_spec(kind).stratum_offset;
let target_basis = Basis {
depth: target,
strata_depth: target,
..test_basis()
};
let shallow_basis = Basis {
depth: 0.20,
strata_depth: 0.20,
..test_basis()
};
assert!(
material_score(kind, target_basis) > material_score(kind, shallow_basis) + 0.7,
"{kind:?} should strongly prefer its deep target stratum"
);
assert!(
material_score(kind, target_basis)
> material_score(CellKind::Aetherite, target_basis),
"{kind:?} should beat the shallow mythical fallback at its target stratum"
);
}
for (kind, target) in [
(CellKind::Snorgite, -0.35),
(CellKind::Bonkrock, -0.05),
(CellKind::SuspiciouslySlimeyRock, -0.15),
] {
let target_basis = Basis {
depth: target,
strata_depth: target,
..test_basis()
};
let next_best_existing_prize = [
CellKind::AncientBronze,
CellKind::MeteoricSilver,
CellKind::SolidGold,
CellKind::GildedDiamond,
]
.into_iter()
.map(|existing| material_score(existing, target_basis))
.fold(f32::NEG_INFINITY, f32::max);
assert!(
material_score(kind, target_basis) > next_best_existing_prize,
"{kind:?} should be able to win early sparse prize deposits"
);
}
}
#[test]
fn generator_is_deterministic() {
let config = GridConfig::default();
let coord = IVec2::new(3, -9);
let local = UVec2::new(7, 11);
assert_eq!(
generate_cell_kind(&config, coord, local),
generate_cell_kind(&config, coord, local)
);
}
#[test]
fn generated_chunks_do_not_start_with_empty_cells() {
let config = GridConfig::default();
let cells = build_generated_cells(&config, IVec2::ZERO);
assert_eq!(cells.len(), config.tiles_per_chunk());
assert!(cells.iter().all(|kind| *kind != CellKind::Empty));
}
#[test]
fn chunk_generation_matches_direct_cell_generation() {
let config = GridConfig::default();
let chunk = IVec2::new(-3, 5);
let cells = build_generated_cells(&config, chunk);
for local in [
UVec2::new(0, 0),
UVec2::new(7, 11),
UVec2::new(config.chunk_side as u32 - 1, config.chunk_side as u32 - 1),
] {
assert_eq!(
cells[tile_index(&config, local)],
generate_cell_kind(&config, chunk, local)
);
}
}
#[test]
fn simd_basis_row_matches_scalar_basis_fields() {
let seed = GridConfig::default().world_seed;
for cell_start in [
IVec2::new(-19, -37),
IVec2::new(0, 0),
IVec2::new(113, -241),
IVec2::new(337, 19),
] {
let cell_x = row_cell_x(cell_start);
let cell_y = row_cell_y(cell_start);
let intrusion_simd = intrusion_proximity_simd(seed, cell_x, cell_y);
let intrusions = intrusion_simd.to_array();
let row = BasisRow::from_simd(basis_fields_simd(seed, cell_x, cell_y, intrusion_simd));
for (lane, intrusion) in intrusions.into_iter().enumerate() {
let cell = cell_start + IVec2::new(lane as i32, 0);
let actual = row.lane(lane);
let expected = basis_fields_with_intrusion(seed, cell, intrusion);
assert_basis_close(actual, expected, cell);
}
}
}
#[test]
fn simd_intrusion_row_matches_scalar_proximity() {
let seed = GridConfig::default().world_seed;
for cell_start in [
IVec2::new(-343, -37),
IVec2::new(-8, 0),
IVec2::new(0, 0),
IVec2::new(337, -241),
IVec2::new(678, 19),
] {
let row =
intrusion_proximity_simd(seed, row_cell_x(cell_start), row_cell_y(cell_start))
.to_array();
for (lane, actual) in row.into_iter().enumerate() {
let cell = cell_start + IVec2::new(lane as i32, 0);
let expected = intrusion_proximity(seed, cell);
assert!(
(actual - expected).abs() <= 1.0e-6,
"intrusion drifted at {cell:?}: actual={actual}, expected={expected}"
);
}
}
}
#[test]
fn simd_deposit_feature_indices_match_scalar_lookup() {
let config = GridConfig::default();
for chunk in [
IVec2::new(-3, 5),
IVec2::new(0, 0),
IVec2::new(2, -7),
IVec2::new(21, -2),
] {
let features = precompute_chunk_features(config.world_seed, chunk, config.chunk_side);
for y in [0, 7, 15] {
for x in [0, 8] {
let cell_start =
global_cell_coord(&config, chunk, UVec2::new(x as u32, y as u32));
let indices = deposit_feature_indices_row(
&features,
row_cell_x(cell_start),
row_cell_y(cell_start),
);
for (lane, actual) in indices.into_iter().enumerate() {
let cell = cell_start + IVec2::new(lane as i32, 0);
let expected = deposit_feature_index_from(&features, cell);
assert_eq!(actual, expected, "deposit index drifted at {cell:?}");
}
}
}
}
}
#[test]
fn group_priors_shift_deeper() {
let seed = GridConfig::default().world_seed;
let shallow = Basis {
depth: -0.35,
strata_depth: -0.35,
limestone: 1.0,
temperature: -0.6,
lo_fbm: 0.6,
..test_basis()
};
let deep = Basis {
depth: 1.45,
strata_depth: 1.45,
intrusion: 0.8,
fracture: 0.7,
temperature: 0.7,
hi_fbm: 0.5,
..test_basis()
};
let shallow_groups = group_samples(seed, IVec2::ZERO, shallow);
let deep_groups = group_samples(seed, IVec2::ZERO, deep);
let shallow_front = shallow_groups[MaterialGroup::Carbon.index()].logit
+ shallow_groups[MaterialGroup::BaseMetal.index()].logit;
let shallow_deep = shallow_groups[MaterialGroup::RareMetal.index()].logit
+ shallow_groups[MaterialGroup::RadioactiveMetal.index()].logit;
let deep_front = deep_groups[MaterialGroup::Carbon.index()].logit
+ deep_groups[MaterialGroup::BaseMetal.index()].logit;
let deep_exotic = deep_groups[MaterialGroup::RareMetal.index()].logit
+ deep_groups[MaterialGroup::RadioactiveMetal.index()].logit
+ deep_groups[MaterialGroup::RefractoryMetal.index()].logit;
assert!(shallow_front > shallow_deep);
assert!(deep_exotic > deep_front);
}
#[test]
fn texture_amplitude_is_feature_gated() {
let seed = GridConfig::default().world_seed;
let bulk = Basis {
depth: 1.8,
strata_depth: 1.8,
..test_basis()
};
let gold_zone = Basis {
depth: 0.72,
strata_depth: 0.52,
ridge: 1.0,
intrusion: 1.0,
fracture: 1.0,
temperature: 1.0,
hi_fbm: 0.5,
..test_basis()
};
let coal_zone = Basis {
depth: -0.30,
strata_depth: -0.52,
lo_fbm: 1.0,
temperature: -1.0,
limestone: 1.0,
..test_basis()
};
let bulk_groups = group_samples(seed, IVec2::ZERO, bulk);
let gold_groups = group_samples(seed, IVec2::ZERO, gold_zone);
let coal_groups = group_samples(seed, IVec2::ZERO, coal_zone);
assert_eq!(
bulk_groups[MaterialGroup::Rock.index()].texture_amplitude,
0.0
);
assert!(
gold_groups[MaterialGroup::PreciousMetal.index()].texture_amplitude
> bulk_groups[MaterialGroup::PreciousMetal.index()].texture_amplitude * 4.0
);
assert!(
coal_groups[MaterialGroup::Carbon.index()].texture_amplitude
> bulk_groups[MaterialGroup::Carbon.index()].texture_amplitude * 4.0
);
}
#[test]
fn texture_variance_rises_inside_feature_regions() {
let seed = GridConfig::default().world_seed;
let spec = MATERIAL_SPECS[cell_kind_index(CellKind::Gold)];
let bulk = Basis {
depth: 1.8,
strata_depth: 1.8,
..test_basis()
};
let gold_zone = Basis {
depth: 0.72,
strata_depth: 0.52,
ridge: 1.0,
intrusion: 1.0,
fracture: 1.0,
temperature: 1.0,
hi_fbm: 0.5,
..test_basis()
};
let mut bulk_terms = Vec::with_capacity(128);
let mut zone_terms = Vec::with_capacity(128);
for index in 0..128 {
let cell = IVec2::new(index % 32, index / 32);
let bulk_group = group_samples(seed, cell, bulk)[spec.group.index()];
let zone_group = group_samples(seed, cell, gold_zone)[spec.group.index()];
bulk_terms.push(bulk_group.texture_amplitude * bulk_group.texture_value);
zone_terms.push(zone_group.texture_amplitude * zone_group.texture_value);
}
assert!(
variance(&zone_terms) > variance(&bulk_terms) * 8.0,
"bulk_variance={}, zone_variance={}",
variance(&bulk_terms),
variance(&zone_terms)
);
}
#[test]
fn generated_material_distribution_is_playable() {
let config = GridConfig::default();
let mut counts = [0usize; MATERIAL_COUNT];
let mut total = 0usize;
for chunk_y in -12..=12 {
for chunk_x in -12..=12 {
for kind in build_generated_cells(&config, IVec2::new(chunk_x, chunk_y)) {
counts[cell_kind_index(kind)] += 1;
total += 1;
}
}
}
let host_rock_count: usize = RENDERABLE_CELL_KINDS
.iter()
.filter(|k| is_host_rock(**k))
.map(|k| counts[cell_kind_index(*k)])
.sum();
let host_rock_ratio = host_rock_count as f32 / total as f32;
let ore_ratio = 1.0 - host_rock_ratio;
let meaningful_materials = counts
.iter()
.enumerate()
.filter(|(index, count)| {
let kind = RENDERABLE_CELL_KINDS[*index];
!is_host_rock(kind) && **count as f32 / total as f32 > 0.001
})
.count();
assert!(
(0.10..=0.27).contains(&ore_ratio),
"ore_ratio={ore_ratio}, host_rock_ratio={host_rock_ratio}, counts={counts:?}"
);
assert!(
meaningful_materials >= 18,
"meaningful_materials={meaningful_materials}, counts={counts:?}"
);
}
#[test]
fn generated_ore_rate_stays_bounded_across_depth_bands() {
let config = GridConfig::default();
let bands = [
(-46, -36),
(-28, -18),
(-10, 0),
(8, 18),
(26, 36),
(40, 50),
];
let mut ratios = Vec::with_capacity(bands.len());
for (min_chunk_y, max_chunk_y) in bands {
let mut ore_cells = 0usize;
let mut total_cells = 0usize;
for chunk_y in min_chunk_y..=max_chunk_y {
for chunk_x in -12..=12 {
for kind in build_generated_cells(&config, IVec2::new(chunk_x, chunk_y)) {
ore_cells += is_ore(kind) as usize;
total_cells += 1;
}
}
}
let ratio = ore_cells as f32 / total_cells as f32;
ratios.push(ratio);
assert!(
(0.02..=0.26).contains(&ratio),
"band=({min_chunk_y}, {max_chunk_y}), ratio={ratio}, ratios={ratios:?}"
);
}
let min_ratio = ratios.iter().copied().fold(f32::INFINITY, f32::min);
let max_ratio = ratios.iter().copied().fold(f32::NEG_INFINITY, f32::max);
assert!(
max_ratio - min_ratio < 0.24,
"depth ore ratios diverged too much: {ratios:?}"
);
}
#[test]
fn generated_ores_are_locally_clustered() {
let config = GridConfig::default();
let mut ore_cells = 0usize;
let mut total_cells = 0usize;
let mut ore_neighbor_pairs = 0usize;
let mut neighbor_pairs = 0usize;
for chunk_y in -8..=8 {
for chunk_x in -8..=8 {
let chunk = IVec2::new(chunk_x, chunk_y);
let cells = build_generated_cells(&config, chunk);
for y in 0..config.chunk_side {
for x in 0..config.chunk_side {
let local = UVec2::new(x as u32, y as u32);
let kind = cells[tile_index(&config, local)];
let cell_is_ore = is_ore(kind);
ore_cells += cell_is_ore as usize;
total_cells += 1;
if x + 1 < config.chunk_side {
let neighbor =
cells[tile_index(&config, UVec2::new(x as u32 + 1, y as u32))];
ore_neighbor_pairs += (cell_is_ore && is_ore(neighbor)) as usize;
neighbor_pairs += 1;
}
if y + 1 < config.chunk_side {
let neighbor =
cells[tile_index(&config, UVec2::new(x as u32, y as u32 + 1))];
ore_neighbor_pairs += (cell_is_ore && is_ore(neighbor)) as usize;
neighbor_pairs += 1;
}
}
}
}
}
let ore_ratio = ore_cells as f32 / total_cells as f32;
let ore_neighbor_ratio = ore_neighbor_pairs as f32 / neighbor_pairs as f32;
assert!(
ore_neighbor_ratio > ore_ratio * ore_ratio * 1.35,
"ore_ratio={ore_ratio}, ore_neighbor_ratio={ore_neighbor_ratio}"
);
}
#[test]
fn generated_material_identity_is_coherent_inside_groups() {
let config = GridConfig::default();
let mut same_group_pairs = 0usize;
let mut same_material_pairs = 0usize;
for chunk_y in -8..=8 {
for chunk_x in -8..=8 {
let chunk = IVec2::new(chunk_x, chunk_y);
let cells = build_generated_cells(&config, chunk);
for y in 0..config.chunk_side {
for x in 0..config.chunk_side {
let local = UVec2::new(x as u32, y as u32);
let kind = cells[tile_index(&config, local)];
if x + 1 < config.chunk_side {
let neighbor =
cells[tile_index(&config, UVec2::new(x as u32 + 1, y as u32))];
count_group_pair(
kind,
neighbor,
&mut same_group_pairs,
&mut same_material_pairs,
);
}
if y + 1 < config.chunk_side {
let neighbor =
cells[tile_index(&config, UVec2::new(x as u32, y as u32 + 1))];
count_group_pair(
kind,
neighbor,
&mut same_group_pairs,
&mut same_material_pairs,
);
}
}
}
}
}
let same_material_ratio = same_material_pairs as f32 / same_group_pairs as f32;
assert!(
same_material_ratio > 0.55,
"same_group_pairs={same_group_pairs}, same_material_ratio={same_material_ratio}"
);
}
#[test]
fn generated_material_singletons_are_limited() {
let config = GridConfig::default();
let mut ore_cells = 0usize;
let mut singleton_cells = 0usize;
for chunk_y in -8..=8 {
for chunk_x in -8..=8 {
let chunk = IVec2::new(chunk_x, chunk_y);
let cells = build_generated_cells(&config, chunk);
for y in 1..config.chunk_side - 1 {
for x in 1..config.chunk_side - 1 {
let local = UVec2::new(x as u32, y as u32);
let kind = cells[tile_index(&config, local)];
if !is_ore(kind) {
continue;
}
let has_matching_neighbor = [
UVec2::new(x as u32 - 1, y as u32),
UVec2::new(x as u32 + 1, y as u32),
UVec2::new(x as u32, y as u32 - 1),
UVec2::new(x as u32, y as u32 + 1),
]
.into_iter()
.any(|neighbor| cells[tile_index(&config, neighbor)] == kind);
ore_cells += 1;
singleton_cells += (!has_matching_neighbor) as usize;
}
}
}
}
let singleton_ratio = singleton_cells as f32 / ore_cells as f32;
assert!(
singleton_ratio < 0.42,
"ore_cells={ore_cells}, singleton_ratio={singleton_ratio}"
);
}
#[test]
fn gumbel_noise_is_finite() {
let value = gumbel(123, 0xDEAD_BEEF, 2);
assert!(value.is_finite());
}
#[test]
#[ignore]
fn bench_build_generated_cells() {
const N_CHUNKS: u32 = 400;
const REGRESSION_GUARD_PER_CHUNK: std::time::Duration =
std::time::Duration::from_millis(50);
let config = GridConfig::default();
for cy in -2..=2 {
for cx in -2..=2 {
std::hint::black_box(build_generated_cells(&config, IVec2::new(cx, cy)));
}
}
let start = std::time::Instant::now();
for i in 0..N_CHUNKS {
let cy = (i as i32 / 20) - 10;
let cx = (i as i32 % 20) - 10;
std::hint::black_box(build_generated_cells(&config, IVec2::new(cx, cy)));
}
let elapsed = start.elapsed();
let per_chunk = elapsed / N_CHUNKS;
println!("{N_CHUNKS} chunks in {elapsed:?} = {per_chunk:?}/chunk");
assert!(
per_chunk < REGRESSION_GUARD_PER_CHUNK,
"chunk gen regressed past {REGRESSION_GUARD_PER_CHUNK:?}/chunk: {per_chunk:?}",
);
}
fn generate_cell_kind(config: &GridConfig, chunk: IVec2, local: UVec2) -> CellKind {
build_generated_cells(config, chunk)[tile_index(config, local)]
}
fn cell_kind_index(kind: CellKind) -> usize {
kind.tileset_index().map_or(0, usize::from)
}
fn material_spec(kind: CellKind) -> MaterialSpec {
MATERIAL_SPECS[cell_kind_index(kind)]
}
fn material_score(kind: CellKind, basis: Basis) -> f32 {
material_identity_logit(material_spec(kind), basis)
}
fn is_ore(kind: CellKind) -> bool {
kind != CellKind::Empty && !is_host_rock(kind)
}
fn is_host_rock(kind: CellKind) -> bool {
matches!(
kind.material_group(),
MaterialGroup::Rock | MaterialGroup::Sedimentary
)
}
fn count_group_pair(
a: CellKind,
b: CellKind,
same_group_pairs: &mut usize,
same_material_pairs: &mut usize,
) {
if !is_ore(a) || !is_ore(b) || a.material_group() != b.material_group() {
return;
}
*same_group_pairs += 1;
*same_material_pairs += (a == b) as usize;
}
fn test_basis() -> Basis {
Basis {
depth: 0.0,
strata_depth: 0.0,
lo_fbm: 0.0,
hi_fbm: 0.0,
ridge: 0.0,
intrusion: 0.0,
fracture: 0.0,
temperature: 0.0,
limestone: 0.0,
}
}
fn assert_basis_close(actual: Basis, expected: Basis, cell: IVec2) {
for (label, actual, expected) in [
("depth", actual.depth, expected.depth),
("strata_depth", actual.strata_depth, expected.strata_depth),
("lo_fbm", actual.lo_fbm, expected.lo_fbm),
("hi_fbm", actual.hi_fbm, expected.hi_fbm),
("ridge", actual.ridge, expected.ridge),
("intrusion", actual.intrusion, expected.intrusion),
("fracture", actual.fracture, expected.fracture),
("temperature", actual.temperature, expected.temperature),
("limestone", actual.limestone, expected.limestone),
] {
assert!(
(actual - expected).abs() <= 1.0e-6,
"{label} drifted at {cell:?}: actual={actual}, expected={expected}"
);
}
}
fn variance(values: &[f32]) -> f32 {
let mean = values.iter().sum::<f32>() / values.len() as f32;
values
.iter()
.map(|value| {
let delta = value - mean;
delta * delta
})
.sum::<f32>()
/ values.len() as f32
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment