Created
May 6, 2026 17:02
-
-
Save CoffeeVampir3/8a4788aa14274fbf37073c2528a1adf7 to your computer and use it in GitHub Desktop.
Gumbel-max based spatially covariant feature-gated sampling + correction
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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