From cccdf53e7c9b5c2c6dacd8f912e102a7ab6c47c5 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Tue, 4 Mar 2025 10:07:06 +0100 Subject: [PATCH] Add Multi noise (#597) * Copy @urisinger work * Fix compile time errors * fix clippy --- pumpkin-data/build/biome.rs | 17 +- pumpkin-world/Cargo.toml | 3 +- pumpkin-world/src/biome.rs | 25 -- pumpkin-world/src/biome/mod.rs | 55 +++ pumpkin-world/src/biome/multi_noise.rs | 327 ++++++++++++++++++ pumpkin-world/src/dimension.rs | 5 +- .../noise_router/multi_noise_sampler.rs | 27 +- 7 files changed, 417 insertions(+), 42 deletions(-) delete mode 100644 pumpkin-world/src/biome.rs create mode 100644 pumpkin-world/src/biome/mod.rs create mode 100644 pumpkin-world/src/biome/multi_noise.rs diff --git a/pumpkin-data/build/biome.rs b/pumpkin-data/build/biome.rs index 190e044cb..95348cb04 100644 --- a/pumpkin-data/build/biome.rs +++ b/pumpkin-data/build/biome.rs @@ -1,17 +1,24 @@ +use heck::ToPascalCase; use proc_macro2::TokenStream; -use quote::quote; - -use crate::array_to_tokenstream; +use quote::{format_ident, quote}; pub(crate) fn build() -> TokenStream { println!("cargo:rerun-if-changed=../assets/biome.json"); let biomes: Vec = serde_json::from_str(include_str!("../../assets/biome.json")) .expect("Failed to parse biome.json"); - let variants = array_to_tokenstream(&biomes); + let mut variants = TokenStream::new(); + for status in biomes.iter() { + let full_name = format!("minecraft:{status}"); + let name = format_ident!("{}", status.to_pascal_case()); + variants.extend([quote! { + #[serde(rename = #full_name)] + #name, + }]); + } quote! { - #[derive(Clone, Copy)] + #[derive(Clone, Deserialize, Copy, Hash, PartialEq, Eq)] pub enum Biome { #variants } diff --git a/pumpkin-world/Cargo.toml b/pumpkin-world/Cargo.toml index e6b88528d..9e401278d 100644 --- a/pumpkin-world/Cargo.toml +++ b/pumpkin-world/Cargo.toml @@ -40,8 +40,7 @@ enum_dispatch = "0.3" noise = "0.9" -# Had to use custom, because google's is broken, I made a PR. -serde_json5 = { git = "https://github.com/kralverde/serde_json5.git" } +serde_json5 = "0.2.0" derive-getters = "0.5.0" [dev-dependencies] diff --git a/pumpkin-world/src/biome.rs b/pumpkin-world/src/biome.rs deleted file mode 100644 index fbe1f3a7b..000000000 --- a/pumpkin-world/src/biome.rs +++ /dev/null @@ -1,25 +0,0 @@ -use enum_dispatch::enum_dispatch; -use pumpkin_data::chunk::Biome; - -#[derive(Clone)] -#[enum_dispatch(BiomeSupplierImpl)] -pub enum BiomeSupplier { - Debug(DebugBiomeSupplier), -} - -#[enum_dispatch] -pub trait BiomeSupplierImpl { - fn biome(&self, x: i32, y: i32, z: i32, noise: &MultiNoiseSampler) -> Biome; -} - -#[derive(Clone)] -pub struct DebugBiomeSupplier {} - -impl BiomeSupplierImpl for DebugBiomeSupplier { - fn biome(&self, _x: i32, _y: i32, _z: i32, _noise: &MultiNoiseSampler) -> Biome { - Biome::Plains - } -} - -// TODO: Implement -pub struct MultiNoiseSampler {} diff --git a/pumpkin-world/src/biome/mod.rs b/pumpkin-world/src/biome/mod.rs new file mode 100644 index 000000000..777646352 --- /dev/null +++ b/pumpkin-world/src/biome/mod.rs @@ -0,0 +1,55 @@ +use std::{cell::RefCell, sync::LazyLock}; + +use enum_dispatch::enum_dispatch; +use multi_noise::{BiomeEntries, SearchTree, TreeLeafNode}; +use pumpkin_data::chunk::Biome; + +use crate::{ + coordinates::BlockCoordinates, generation::noise_router::multi_noise_sampler::MultiNoiseSampler, +}; +pub mod multi_noise; + +pub static BIOME_ENTRIES: LazyLock> = LazyLock::new(|| { + SearchTree::create( + serde_json::from_str::(include_str!("../../../assets/multi_noise.json")) + .expect("Could not parse multi_noise.json.") + .nodes + .into_iter() + .flat_map(|(_, biome_map)| biome_map.into_iter()) + .collect(), + ) + .expect("entries cannot be empty") +}); + +thread_local! { + static LAST_RESULT_NODE: RefCell>> = const {RefCell::new(None) }; +} + +#[enum_dispatch] +pub trait BiomeSupplier { + fn biome(&mut self, at: BlockCoordinates) -> Biome; +} + +#[derive(Clone)] +pub struct DebugBiomeSupplier; + +impl BiomeSupplier for DebugBiomeSupplier { + fn biome(&mut self, _at: BlockCoordinates) -> Biome { + Biome::Plains + } +} + +pub struct MultiNoiseBiomeSupplier<'a> { + noise: MultiNoiseSampler<'a>, +} + +impl BiomeSupplier for MultiNoiseBiomeSupplier<'_> { + fn biome(&mut self, at: BlockCoordinates) -> Biome { + let point = self.noise.sample(at.x, at.y.0 as i32, at.z); + LAST_RESULT_NODE.with_borrow_mut(|last_result| { + BIOME_ENTRIES + .get(&point, last_result) + .expect("failed to get biome entry") + }) + } +} diff --git a/pumpkin-world/src/biome/multi_noise.rs b/pumpkin-world/src/biome/multi_noise.rs new file mode 100644 index 000000000..462a2d8e6 --- /dev/null +++ b/pumpkin-world/src/biome/multi_noise.rs @@ -0,0 +1,327 @@ +use std::{cmp::Ordering, collections::HashMap}; + +use pumpkin_data::chunk::Biome; +use serde::{Deserialize, Deserializer, Serialize}; + +use crate::dimension::Dimension; +pub fn to_long(float: f32) -> i64 { + (float * 1000.0) as i64 +} +#[derive(Clone, Serialize, Deserialize)] +pub struct NoiseValuePoint { + pub temperature: i64, + pub humidity: i64, + pub continentalness: i64, + pub erosion: i64, + pub depth: i64, + pub weirdness: i64, +} + +#[derive(Clone, Deserialize)] +pub struct NoiseHypercube { + pub temperature: ParameterRange, + pub erosion: ParameterRange, + pub depth: ParameterRange, + pub continentalness: ParameterRange, + pub weirdness: ParameterRange, + pub humidity: ParameterRange, + pub offset: i64, +} + +impl NoiseHypercube { + pub fn to_parameters(&self) -> [ParameterRange; 7] { + [ + self.temperature, + self.humidity, + self.continentalness, + self.erosion, + self.depth, + self.weirdness, + ParameterRange { + min: self.offset, + max: self.offset, + }, + ] + } +} + +#[derive(Clone, Copy)] +pub struct ParameterRange { + pub min: i64, + pub max: i64, +} + +impl<'de> Deserialize<'de> for ParameterRange { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let arr: [f32; 2] = Deserialize::deserialize(deserializer)?; + Ok(ParameterRange { + min: to_long(arr[0]), + max: to_long(arr[1]), + }) + } +} + +impl ParameterRange { + fn get_distance(&self, noise: i64) -> i64 { + let l = noise - self.max; + let m = self.min - noise; + if l > 0 { l } else { m.max(0) } + } + + pub fn combine(&self, other: &Self) -> Self { + Self { + min: self.min.min(other.min), + max: self.max.max(other.max), + } + } +} + +#[derive(Clone, Deserialize)] +pub struct BiomeEntries { + pub nodes: HashMap>, +} + +#[derive(Clone)] +/// T = Biome +pub struct SearchTree { + root: TreeNode, +} + +impl SearchTree { + pub fn create(entries: Vec<(T, NoiseHypercube)>) -> Option { + if entries.is_empty() { + return None; + } + + let leaves: Vec> = entries + .into_iter() + .map(|(value, hypercube)| TreeNode::new_leaf(value, hypercube.to_parameters())) + .collect(); + + Some(SearchTree { + root: create_node(leaves), + }) + } + + pub fn get( + &self, + point: &NoiseValuePoint, + last_result_node: &mut Option>, + ) -> Option { + let point = &[ + point.temperature, + point.humidity, + point.continentalness, + point.erosion, + point.depth, + point.weirdness, + 0, + ]; + let result_node = self.root.get_node(point, last_result_node); + let result = result_node.clone().map(|node| node.value); + *last_result_node = result_node; + result + } +} + +fn create_node(sub_tree: Vec>) -> TreeNode { + if sub_tree.is_empty() { + panic!("Need at least one child to build a node"); + } else if sub_tree.len() == 1 { + sub_tree.into_iter().next().unwrap() + } else if sub_tree.len() <= 6 { + let mut sorted_sub_tree = sub_tree; + sorted_sub_tree.sort_by_key(|a| calculate_midpoint_sum(a)); + let bounds = calculate_bounds(&sorted_sub_tree); + TreeNode::Branch { + children: sorted_sub_tree, + bounds, + } + } else { + let best_split = (0..7) + .map(|param_idx| { + let mut sorted_sub_tree = sub_tree.clone(); + sort_tree(&mut sorted_sub_tree, param_idx, false); + let batched_tree = get_batched_tree(sorted_sub_tree); + + let range_sum: i64 = batched_tree + .iter() + .map(|node| calculate_bounds_sum(node.bounds())) + .sum(); + + (param_idx, batched_tree, range_sum) + }) + .min_by_key(|(_, _, range_sum)| *range_sum) + .unwrap(); + + let (best_param, mut best_batched, _) = best_split; + sort_tree(&mut best_batched, best_param, true); + let children: Vec> = best_batched + .into_iter() + .map(|batch| create_node(batch.children().to_vec())) + .collect(); + let bounds = calculate_bounds(&children); + TreeNode::Branch { children, bounds } + } +} + +fn sort_tree(sub_tree: &mut [TreeNode], parameter_offset: usize, abs: bool) { + sub_tree.sort_by(|a, b| { + for i in 0..7 { + // Calculate the parameter index in cyclic order + let current_param = (parameter_offset + i) % 7; + + // Get the midpoints for the current parameter + let mid_a = get_midpoint(a, current_param); + let mid_b = get_midpoint(b, current_param); + + // Apply absolute value if required + let val_a = if abs { mid_a.abs() } else { mid_a }; + let val_b = if abs { mid_b.abs() } else { mid_b }; + + match val_a.cmp(&val_b) { + Ordering::Equal => continue, // Move to the next parameter if equal + non_equal => return non_equal, // Return the result if not equal + } + } + + Ordering::Equal // All parameters are equal + }); +} + +fn get_midpoint(node: &TreeNode, parameter: usize) -> i64 { + let range = &node.bounds()[parameter]; + (range.min + range.max) / 2 +} + +fn calculate_midpoint_sum(node: &TreeNode) -> i64 { + node.bounds() + .iter() + .map(|range| ((range.min + range.max) / 2).abs()) + .sum() +} + +fn get_batched_tree(nodes: Vec>) -> Vec> { + let mut result = Vec::new(); + let mut current_batch = Vec::new(); + + // Calculate batch size based on the formula + let node_count = nodes.len(); + let batch_size = (6.0f64.powf((node_count as f64 - 0.01).log(6.0).floor())) as usize; + + for node in nodes { + current_batch.push(node); + + if current_batch.len() >= batch_size { + result.push(TreeNode::Branch { + children: current_batch.clone(), + bounds: calculate_bounds(¤t_batch), + }); + current_batch.clear(); + } + } + + // Add the remaining nodes as the final batch + if !current_batch.is_empty() { + result.push(TreeNode::Branch { + children: current_batch.clone(), + bounds: calculate_bounds(¤t_batch), + }); + } + + result +} + +fn calculate_bounds(nodes: &[TreeNode]) -> [ParameterRange; 7] { + let mut bounds = *nodes[0].bounds(); + + for node in nodes.iter().skip(1) { + for (i, range) in node.bounds().iter().enumerate() { + bounds[i] = bounds[i].combine(range); + } + } + + bounds +} + +fn calculate_bounds_sum(bounds: &[ParameterRange]) -> i64 { + bounds.iter().map(|range| range.max - range.min).sum() +} + +#[derive(Clone)] +pub enum TreeNode { + Leaf(TreeLeafNode), + Branch { + children: Vec>, + bounds: [ParameterRange; 7], + }, +} + +#[derive(Clone)] +pub struct TreeLeafNode { + value: T, + point: [ParameterRange; 7], +} + +impl TreeNode { + pub fn new_leaf(value: T, point: [ParameterRange; 7]) -> Self { + TreeNode::Leaf(TreeLeafNode { value, point }) + } + + // pub fn new_branch(children: Vec>, bounds: [ParameterRange; 7]) -> Self { + // TreeNode::Branch { children, bounds } + // } + + pub fn get_node( + &self, + point: &[i64; 7], + alternative: &Option>, + ) -> Option> { + match self { + Self::Leaf(node) => Some(node.clone()), + Self::Branch { children, .. } => { + let mut min = alternative + .as_ref() + .map(|node| squared_distance(&node.point, point)) + .unwrap_or(i64::MAX); + let mut tree_leaf_node = alternative.clone(); + for node in children { + let distance = squared_distance(node.bounds(), point); + if distance < min { + let tree_leaf_node2 = node + .get_node(point, alternative) + .expect("get_node should always return a value on a non empty tree"); + + let distance2 = squared_distance(&tree_leaf_node2.point, point); + if distance2 < min { + min = distance2; + tree_leaf_node = Some(tree_leaf_node2); + } + } + } + tree_leaf_node + } + } + } + + pub fn bounds(&self) -> &[ParameterRange; 7] { + match self { + TreeNode::Leaf(TreeLeafNode { point, .. }) => point, + TreeNode::Branch { bounds, .. } => bounds, + } + } + + pub fn children(self) -> Vec> { + match self { + TreeNode::Leaf(TreeLeafNode { .. }) => vec![], + TreeNode::Branch { children, .. } => children, + } + } +} + +fn squared_distance(a: &[ParameterRange; 7], b: &[i64; 7]) -> i64 { + a.iter().zip(b).map(|(a, b)| a.get_distance(*b)).sum() +} diff --git a/pumpkin-world/src/dimension.rs b/pumpkin-world/src/dimension.rs index 31fe399c9..7d1728b06 100644 --- a/pumpkin-world/src/dimension.rs +++ b/pumpkin-world/src/dimension.rs @@ -1,8 +1,11 @@ use std::path::PathBuf; +use serde::Deserialize; + use crate::level::Level; -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum Dimension { OverWorld, Nether, diff --git a/pumpkin-world/src/generation/noise_router/multi_noise_sampler.rs b/pumpkin-world/src/generation/noise_router/multi_noise_sampler.rs index ee01e7510..09691b62d 100644 --- a/pumpkin-world/src/generation/noise_router/multi_noise_sampler.rs +++ b/pumpkin-world/src/generation/noise_router/multi_noise_sampler.rs @@ -1,5 +1,7 @@ use crate::{ - GlobalProtoNoiseRouter, generation::biome_coords, + GlobalProtoNoiseRouter, + biome::multi_noise::{NoiseValuePoint, to_long}, + generation::biome_coords, noise_router::density_function_ast::WrapperType, }; @@ -45,7 +47,7 @@ pub struct MultiNoiseSampler<'a> { } impl<'a> MultiNoiseSampler<'a> { - pub fn sample(&mut self, biome_x: i32, biome_y: i32, biome_z: i32) { + pub fn sample(&mut self, biome_x: i32, biome_y: i32, biome_z: i32) -> NoiseValuePoint { let block_x = biome_coords::to_block(biome_x); let block_y = biome_coords::to_block(biome_y); let block_z = biome_coords::to_block(biome_z); @@ -54,43 +56,50 @@ impl<'a> MultiNoiseSampler<'a> { let sample_options = ChunkNoiseFunctionSampleOptions::new(false, SampleAction::SkipCellCaches, 0, 0, 0); - let _temperature = ChunkNoiseFunctionComponent::sample_from_stack( + let temperature = ChunkNoiseFunctionComponent::sample_from_stack( &mut self.component_stack[..=self.temperature], &pos, &sample_options, ) as f32; - let _humidity = ChunkNoiseFunctionComponent::sample_from_stack( + let humidity = ChunkNoiseFunctionComponent::sample_from_stack( &mut self.component_stack[..=self.vegetation], &pos, &sample_options, ) as f32; - let _continentalness = ChunkNoiseFunctionComponent::sample_from_stack( + let continentalness = ChunkNoiseFunctionComponent::sample_from_stack( &mut self.component_stack[..=self.continents], &pos, &sample_options, ) as f32; - let _erosion = ChunkNoiseFunctionComponent::sample_from_stack( + let erosion = ChunkNoiseFunctionComponent::sample_from_stack( &mut self.component_stack[..=self.erosion], &pos, &sample_options, ) as f32; - let _depth = ChunkNoiseFunctionComponent::sample_from_stack( + let depth = ChunkNoiseFunctionComponent::sample_from_stack( &mut self.component_stack[..=self.depth], &pos, &sample_options, ) as f32; - let _weirdness = ChunkNoiseFunctionComponent::sample_from_stack( + let weirdness = ChunkNoiseFunctionComponent::sample_from_stack( &mut self.component_stack[..=self.ridges], &pos, &sample_options, ) as f32; - // TODO: Multi noise value here + NoiseValuePoint { + temperature: to_long(temperature), + humidity: to_long(humidity), + continentalness: to_long(continentalness), + erosion: to_long(erosion), + depth: to_long(depth), + weirdness: to_long(weirdness), + } } pub fn generate(