From 767731ecb01bd99cf94702f53e23a3316a9e2ca3 Mon Sep 17 00:00:00 2001 From: Syniis Date: Sat, 2 Mar 2024 23:31:42 +0100 Subject: [PATCH] Cave performance improvements --- Cargo.lock | 15 ++ world/Cargo.toml | 6 +- world/benches/cave.rs | 67 +++++++ world/src/layer/cave.rs | 373 +++++++++++++++++++----------------- world/src/util/structure.rs | 52 +++++ 5 files changed, 332 insertions(+), 181 deletions(-) create mode 100644 world/benches/cave.rs diff --git a/Cargo.lock b/Cargo.lock index ecc5992933..433f4fbedc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3166,8 +3166,22 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6acddbefae08bfba73e27f55513f491f35c365d84bf3002bf85ba9b916c5e5f" dependencies = [ + "inline_tweak_derive", "lazy_static", + "proc-macro2 1.0.78", "rustc-hash", + "syn 2.0.48", +] + +[[package]] +name = "inline_tweak_derive" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46d62a0a3b6af04d4eee8e7251cd758ce74b0ed86253d3e4ac8a1b297a75f4a0" +dependencies = [ + "proc-macro2 1.0.78", + "quote 1.0.35", + "syn 2.0.48", ] [[package]] @@ -7442,6 +7456,7 @@ dependencies = [ "hashbrown 0.13.2", "image", "indicatif", + "inline_tweak", "itertools 0.10.5", "kiddo", "lazy_static", diff --git a/world/Cargo.toml b/world/Cargo.toml index 6e90ae3ebe..a392968c1d 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -40,7 +40,7 @@ packed_simd = { version = "0.3.9", optional = true } rayon = { workspace = true } serde = { workspace = true } ron = { workspace = true } -# inline_tweak = { workspace = true, features = ["derive"] } +inline_tweak = { workspace = true, features = ["derive"] } kiddo = "0.2" strum = { workspace = true } @@ -69,6 +69,10 @@ svg_fmt = "0.4" harness = false name = "tree" +[[bench]] +harness = false +name = "cave" + [[example]] name = "chunk_compression_benchmarks" required-features = ["bin_compression"] diff --git a/world/benches/cave.rs b/world/benches/cave.rs new file mode 100644 index 0000000000..b8f0ec2e1e --- /dev/null +++ b/world/benches/cave.rs @@ -0,0 +1,67 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use rayon::ThreadPoolBuilder; +use vek::Vec2; +use veloren_world::{ + layer, + sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP}, + Land, World, +}; + +fn cave(c: &mut Criterion) { + let pool = ThreadPoolBuilder::new().build().unwrap(); + let (world, index) = World::generate( + 123, + WorldOpts { + seed_elements: true, + world_file: FileOpts::LoadAsset(DEFAULT_WORLD_MAP.into()), + ..WorldOpts::default() + }, + &pool, + &|_| {}, + ); + let land = Land::from_sim(world.sim()); + + c.bench_function("generate", |b| { + b.iter(|| { + let entrances = black_box(layer::cave::surface_entrances(&land)) + .step_by(5) + .map(|e| e / 32); + for entrance in entrances { + _ = black_box(world.generate_chunk( + index.as_index_ref(), + entrance, + None, + || false, + None, + )); + } + }); + }); + + c.bench_function("generate_specific", |b| { + b.iter(|| { + let base_positions = vec![ + Vec2::new(600, 650), + Vec2::new(630, 300), + Vec2::new(809, 141), + ]; + for base_pos in base_positions { + for i in 0..=4 { + for j in 0..=4 { + let pos = base_pos + Vec2::new(i as i32, j as i32) - 2; + _ = black_box(world.generate_chunk( + index.as_index_ref(), + pos, + None, + || false, + None, + )); + } + } + } + }); + }); +} + +criterion_group!(benches, cave); +criterion_main!(benches); diff --git a/world/src/layer/cave.rs b/world/src/layer/cave.rs index a7ce750ff9..63e41fa1f5 100644 --- a/world/src/layer/cave.rs +++ b/world/src/layer/cave.rs @@ -174,6 +174,7 @@ impl Tunnel { } } + #[inline_tweak::tweak_fn] fn biome_at(&self, wpos: Vec3, info: &CanvasInfo) -> Biome { let Some(col) = info.col_or_gen(wpos.xy()) else { return Biome::default(); @@ -202,7 +203,7 @@ impl Tunnel { .mul(2.0) .sub(1.0) .add( - ((col.alt - wpos.z as f32) / (AVG_LEVEL_DEPTH as f32 * LAYERS as f32 * 0.75)) + ((col.alt - wpos.z as f32) / (AVG_LEVEL_DEPTH as f32 * LAYERS as f32 * 0.6)) .clamped(0.0, 2.5), ), below, @@ -233,38 +234,38 @@ impl Tunnel { // Mushrooms grow underground and thrive in a humid environment with moderate // temperatures let mushroom = underground - * close(humidity, 1.0, 0.7, 3) - * close(temp, 1.5, 0.9, 3) - * close(depth, 1.0, 0.6, 3); + * close(humidity, 1.0, 0.7, 4) + * close(temp, 1.5, 0.9, 4) + * close(depth, 1.0, 0.6, 4); // Extremely hot and dry areas deep underground let fire = underground - * close(humidity, 0.0, 0.6, 3) - * close(temp, 2.0, 1.3, 3) - * close(depth, 1.0, 0.55, 3); + * close(humidity, 0.0, 0.6, 4) + * close(temp, 2.0, 1.3, 4) + * close(depth, 1.0, 0.55, 4); // Overgrown with plants that need a moderate climate to survive let leafy = underground - * close(humidity, 0.8, 0.8, 3) - * close(temp, 0.95, 0.85, 3) - * close(depth, 0.0, 0.6, 3); + * close(humidity, 0.8, 0.8, 4) + * close(temp, 0.75, 1.25, 4) + * close(depth, 0.0, 0.75, 4); // Cool temperature, dry and devoid of value - let dusty = close(humidity, 0.0, 0.5, 3) * close(temp, -0.1, 0.6, 3); + let dusty = close(humidity, 0.0, 0.5, 4) * close(temp, -0.1, 0.6, 4); // Deep underground and freezing cold let icy = underground - * close(temp, -1.5, 1.3, 3) - * close(depth, 1.0, 0.65, 3) - * close(humidity, 1.0, 0.7, 3); + * close(temp, -1.5, 1.3, 4) + * close(depth, 1.0, 0.6, 4) + * close(humidity, 1.0, 0.7, 4); // Rocky cold cave that appear near the surface - let snowy = close(temp, -0.6, 0.5, 3) * close(depth, 0.0, 0.45, 3); + let snowy = close(temp, -0.6, 0.5, 4) * close(depth, 0.0, 0.45, 4); // Crystals grow deep underground in areas rich with minerals. They are present // in areas with colder temperatures and low humidity let crystal = underground - * close(humidity, 0.0, 0.5, 3) - * close(temp, -0.6, 0.75, 3) - * close(depth, 1.0, 0.55, 3) - * close(mineral, 2.0, 1.25, 3); + * close(humidity, 0.0, 0.5, 4) + * close(temp, -0.6, 0.75, 4) + * close(depth, 1.0, 0.55, 4) + * close(mineral, 2.0, 1.25, 4); // Hot, dry and shallow let sandy = - close(humidity, 0.0, 0.3, 3) * close(temp, 0.7, 0.9, 3) * close(depth, 0.0, 0.6, 3); + close(humidity, 0.0, 0.3, 4) * close(temp, 0.7, 0.9, 4) * close(depth, 0.0, 0.6, 4); let biomes = [ barren, mushroom, fire, leafy, dusty, icy, snowy, crystal, sandy, @@ -434,6 +435,7 @@ pub fn apply_caves_to(canvas: &mut Canvas, rng: &mut impl Rng) { }) }) .collect::>(); + if !tunnels.is_empty() { let giant_tree_dist = info .chunk @@ -497,6 +499,7 @@ struct Biome { depth: f32, } +#[derive(Clone)] enum CaveStructure { Mushroom(Mushroom), Crystal(CrystalCluster), @@ -508,24 +511,28 @@ enum CaveStructure { }, } +#[derive(Clone)] struct Mushroom { pos: Vec3, stalk: f32, head_color: Rgb, } +#[derive(Clone)] struct Crystal { dir: Vec3, length: f32, radius: f32, } +#[derive(Clone)] struct CrystalCluster { pos: Vec3, crystals: Vec, color: Rgb, } +#[derive(Clone)] struct Flower { pos: Vec3, stalk: f32, @@ -535,6 +542,7 @@ struct Flower { // rotation: Mat3, } +#[inline_tweak::tweak_fn] fn write_column( canvas: &mut Canvas, col: &ColumnSample, @@ -543,7 +551,7 @@ fn write_column( z_range: Range, tunnel: Tunnel, dimensions: (f32, f32, f32), - giant_tree_factor: f32, + giant_tree_dist: f32, structure_cache: &mut SmallCache, Option>, rng: &mut R, ) { @@ -741,154 +749,173 @@ fn write_column( 0 }; - let mut get_structure = |wpos: Vec3, dynamic_rng: &mut R| { - for (wpos2d, seed) in StructureGen2d::new(34537, 24, 8).get(wpos.xy()) { - let structure = if let Some(structure) = - structure_cache.get(wpos2d.with_z(tunnel.a.depth), |_| { - let mut rng = RandomPerm::new(seed); - let (z_range, horizontal, vertical, _) = - tunnel.z_range_at(wpos2d.map(|e| e as f64 + 0.5), info)?; - let pos = wpos2d.with_z(z_range.start); - - let biome = tunnel.biome_at(pos, &info); - let ground_below = !tunnel_bounds_at(pos.xy(), &info, &info.land()) - .any(|(_, z_range, _, _, _, _)| z_range.contains(&(z_range.start - 1))); - if !ground_below { + let structures = StructureGen2d::new(34537, 24, 8) + .get(wpos2d) + .as_slice() + .iter() + .filter_map(|(wpos2d, seed)| { + let structure = structure_cache.get(wpos2d.with_z(tunnel.a.depth), |_| { + let mut rng = RandomPerm::new(*seed); + let (z_range, horizontal, vertical, _) = + tunnel.z_range_at(wpos2d.map(|e| e as f64 + 0.5), info)?; + let pos = wpos2d.with_z(z_range.start); + let biome = tunnel.biome_at(pos, &info); + let tunnel_intersection = || { + tunnel_bounds_at(pos.xy(), &info, &info.land()) + .any(|(_, z_range, _, _, _, _)| z_range.contains(&(z_range.start - 1))) + }; + if biome.mushroom > 0.7 + && vertical > 16.0 + && rng.gen_bool( + 0.5 * close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2) as f64 + * close(biome.mushroom, 1.0, 0.7, 1) as f64, + ) + { + if tunnel_intersection() { return None; } - - if biome.mushroom > 0.7 - && vertical > 16.0 - && rng.gen_bool( - 0.5 * close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2) as f64 - * close(biome.mushroom, 1.0, 0.7, 1) as f64, - ) - { - let purp = rng.gen_range(0..50); - Some(CaveStructure::Mushroom(Mushroom { - pos, - stalk: 8.0 - + rng.gen::().powf(2.0) - * (z_range.end - z_range.start - 8) as f32 - * 0.75, - head_color: Rgb::new( - 40 + purp, - rng.gen_range(60..120), - rng.gen_range(80..200) + purp, - ), - })) - } else if biome.crystal > 0.5 - && rng.gen_bool(0.4 * close(biome.crystal, 1.0, 0.7, 2) as f64) - { - let on_ground = rng.gen_bool(0.6); - let pos = wpos2d.with_z(if on_ground { - z_range.start - } else { - z_range.end - }); - - let mut crystals: Vec = Vec::new(); - let max_length = - (48.0 * close(vertical, MAX_RADIUS, MAX_RADIUS, 1)).max(12.0); - let length = rng.gen_range(8.0..max_length); - let radius = - Lerp::lerp(2.0, 4.5, length / max_length + rng.gen_range(-0.1..0.1)); - let dir = Vec3::new( - rng.gen_range(-3.0..3.0), - rng.gen_range(-3.0..3.0), - rng.gen_range(0.5..10.0) * if on_ground { 1.0 } else { -1.0 }, - ) - .normalized(); - - crystals.push(Crystal { - dir, - length, - radius, - }); - - (0..4).for_each(|_| { - crystals.push(Crystal { - dir: Vec3::new( - rng.gen_range(-1.0..1.0), - rng.gen_range(-1.0..1.0), - (dir.z + rng.gen_range(-0.2..0.2)).clamped(0.0, 1.0), - ), - length: length * rng.gen_range(0.3..0.8), - radius: (radius * rng.gen_range(0.5..0.8)).max(1.0), - }); - }); - - let purple = rng.gen_range(25..75); - let blue = (rng.gen_range(45.0..75.0) * biome.icy) as u8; - - Some(CaveStructure::Crystal(CrystalCluster { - pos, - crystals, - color: Rgb::new( - 255 - blue * 2, - 255 - blue - purple, - 200 + rng.gen_range(25..55), - ), - })) - } else if biome.leafy > 0.8 - && vertical > 16.0 - && horizontal > 8.0 - && rng.gen_bool( - 0.25 * (close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2) - * close(horizontal, MAX_RADIUS, MAX_RADIUS - 8.0, 2) - * biome.leafy) as f64, - ) - { - let petal_radius = rng.gen_range(8.0..16.0); - Some(CaveStructure::Flower(Flower { - pos, - stalk: 6.0 - + rng.gen::().powf(2.0) - * (z_range.end - z_range.start - 8) as f32 - * 0.75, - petals: rng.gen_range(1..5) * 2 + 1, - petal_height: 0.4 * petal_radius * (1.0 + rng.gen::().powf(2.0)), - petal_radius, - })) - } else if (biome.leafy > 0.7 || giant_tree_factor > 0.0) - && rng.gen_bool( - (0.5 * close(biome.leafy, 1.0, 0.5, 1).max(1.0 + giant_tree_factor) - as f64) - .clamped(0.0, 1.0), - ) - { - Some(CaveStructure::GiantRoot { - pos, - radius: rng.gen_range( - 1.5..(3.5 - + close(vertical, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0 - + close(horizontal, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0), - ), - height: (z_range.end - z_range.start) as f32, - }) - } else { - None + let purp = rng.gen_range(0..50); + Some(CaveStructure::Mushroom(Mushroom { + pos, + stalk: 8.0 + + rng.gen::().powf(2.0) + * (z_range.end - z_range.start - 8) as f32 + * 0.75, + head_color: Rgb::new( + 40 + purp, + rng.gen_range(60..120), + rng.gen_range(80..200) + purp, + ), + })) + } else if biome.crystal > 0.5 + && rng.gen_bool(0.4 * close(biome.crystal, 1.0, 0.7, 2) as f64) + { + if tunnel_intersection() { + return None; } - }) { - structure - } else { - continue; - }; + let on_ground = rng.gen_bool(0.6); + let pos = wpos2d.with_z(if on_ground { + z_range.start + } else { + z_range.end + }); + let mut crystals: Vec = Vec::new(); + let max_length = (48.0 * close(vertical, MAX_RADIUS, MAX_RADIUS, 1)).max(12.0); + let length = rng.gen_range(8.0..max_length); + let radius = + Lerp::lerp(2.0, 4.5, length / max_length + rng.gen_range(-0.1..0.1)); + let dir = Vec3::new( + rng.gen_range(-3.0..3.0), + rng.gen_range(-3.0..3.0), + rng.gen_range(0.5..10.0) * if on_ground { 1.0 } else { -1.0 }, + ) + .normalized(); + + crystals.push(Crystal { + dir, + length, + radius, + }); + + (0..4).for_each(|_| { + crystals.push(Crystal { + dir: Vec3::new( + rng.gen_range(-1.0..1.0), + rng.gen_range(-1.0..1.0), + (dir.z + rng.gen_range(-0.2..0.2)).clamped(0.0, 1.0), + ), + length: length * rng.gen_range(0.3..0.8), + radius: (radius * rng.gen_range(0.5..0.8)).max(1.0), + }); + }); + + let purple = rng.gen_range(25..75); + let blue = (rng.gen_range(45.0..75.0) * biome.icy) as u8; + + Some(CaveStructure::Crystal(CrystalCluster { + pos, + crystals, + color: Rgb::new( + 255 - blue * 2, + 255 - blue - purple, + 200 + rng.gen_range(25..55), + ), + })) + } else if biome.leafy > 0.8 + && vertical > 16.0 + && horizontal > 8.0 + && rng.gen_bool( + 0.25 * (close(vertical, MAX_RADIUS, MAX_RADIUS - 16.0, 2) + * close(horizontal, MAX_RADIUS, MAX_RADIUS - 8.0, 2) + * biome.leafy) as f64, + ) + { + if tunnel_intersection() { + return None; + } + let petal_radius = rng.gen_range(8.0..16.0); + Some(CaveStructure::Flower(Flower { + pos, + stalk: 6.0 + + rng.gen::().powf(2.0) + * (z_range.end - z_range.start - 8) as f32 + * 0.75, + petals: rng.gen_range(1..5) * 2 + 1, + petal_height: 0.4 * petal_radius * (1.0 + rng.gen::().powf(2.0)), + petal_radius, + })) + } else if (biome.leafy > 0.7 || giant_tree_dist > 0.0) + && rng.gen_bool( + (0.5 * close(biome.leafy, 1.0, 0.5, 1).max(1.0 + giant_tree_dist) as f64) + .clamped(0.0, 1.0), + ) + { + if tunnel_intersection() { + return None; + } + Some(CaveStructure::GiantRoot { + pos, + radius: rng.gen_range( + 1.5..(3.5 + + close(vertical, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0 + + close(horizontal, MAX_RADIUS, MAX_RADIUS / 2.0, 2) * 3.0), + ), + height: (z_range.end - z_range.start) as f32, + }) + } else { + None + } + }); + + structure + .as_ref() + .map(|structure| (*seed, structure.clone())) + }) + .collect_vec(); + let get_structure = |wpos: Vec3, dynamic_rng: &mut R| { + let warp = |wposf: Vec3, freq: f64, amp: Vec3, seed: u32| -> Option> { + let xy = wposf.xy(); + let xz = Vec2::new(wposf.x, wposf.z); + let yz = Vec2::new(wposf.y, wposf.z); + Some( + Vec3::new( + FastNoise2d::new(seed).get(yz * freq), + FastNoise2d::new(seed).get(xz * freq), + FastNoise2d::new(seed).get(xy * freq), + ) * amp, + ) + }; + for (seed, structure) in structures.iter() { + let seed = *seed; match structure { CaveStructure::Mushroom(mushroom) => { let wposf = wpos.map(|e| e as f64); let warp_freq = 1.0 / 32.0; let warp_amp = Vec3::new(12.0, 12.0, 12.0); - let xy = wposf.xy(); - let xz = Vec2::new(wposf.x, wposf.z); - let yz = Vec2::new(wposf.y, wposf.z); + let warp_offset = warp(wposf, warp_freq, warp_amp, seed)?; let wposf_warped = wposf.map(|e| e as f32) - + Vec3::new( - FastNoise2d::new(seed).get(yz * warp_freq), - FastNoise2d::new(seed).get(xz * warp_freq), - FastNoise2d::new(seed).get(xy * warp_freq), - ) * warp_amp + + warp_offset * (wposf.z as f32 - mushroom.pos.z as f32) .mul(0.1) .clamped(0.0, 1.0); @@ -1011,15 +1038,9 @@ fn write_column( let wposf = wpos.map(|e| e as f64); let warp_freq = 1.0 / 16.0; let warp_amp = Vec3::new(8.0, 8.0, 8.0); - let xy = wposf.xy(); - let xz = Vec2::new(wposf.x, wposf.z); - let yz = Vec2::new(wposf.y, wposf.z); + let warp_offset = warp(wposf, warp_freq, warp_amp, seed)?; let wposf_warped = wposf.map(|e| e as f32) - + Vec3::new( - FastNoise2d::new(seed).get(yz * warp_freq), - FastNoise2d::new(seed).get(xz * warp_freq), - FastNoise2d::new(seed).get(xy * warp_freq), - ) * warp_amp + + warp_offset * (wposf.z as f32 - flower.pos.z as f32) .mul(1.0 / flower.stalk) .sub(1.0) @@ -1044,8 +1065,7 @@ fn write_column( let dist_sq = rpos.xy().magnitude_squared(); let petal_radius_sq = flower.petal_radius.powi(2); if dist_sq < petal_radius_sq { - let petal_height_at = - (dist_sq / petal_radius_sq).powf(1.0) * flower.petal_height; + let petal_height_at = (dist_sq / petal_radius_sq) * flower.petal_height; if rpos.z > petal_height_at - 1.0 && rpos.z <= petal_height_at + petal_thickness { @@ -1108,16 +1128,9 @@ fn write_column( } => { let wposf = wpos.map(|e| e as f64); let warp_freq = 1.0 / 32.0; - let warp_amp = Vec3::new(20.0, 20.0, 20.0); - let xy = wposf.xy(); - let xz = Vec2::new(wposf.x, wposf.z); - let yz = Vec2::new(wposf.y, wposf.z); - let wposf_warped = wposf.map(|e| e as f32) - + Vec3::new( - FastNoise2d::new(seed).get(yz * warp_freq), - FastNoise2d::new(seed).get(xz * warp_freq), - FastNoise2d::new(seed).get(xy * warp_freq), - ) * warp_amp; + let warp_amp = Vec3::new(12.0, 12.0, 12.0); + let warp_offset = warp(wposf, warp_freq, warp_amp, seed)?; + let wposf_warped = wposf.map(|e| e as f32) + warp_offset; let rpos = wposf_warped - pos.map(|e| e as f32); let dist_sq = rpos.xy().magnitude_squared(); if dist_sq < radius.powi(2) { @@ -1142,7 +1155,7 @@ fn write_column( for z in bedrock..z_range.end { let wpos = wpos2d.with_z(z); let mut try_spawn_entity = false; - canvas.map_resource(wpos, |_block| { + canvas.set(wpos, { if z < z_range.start - 4 && !void_below { Block::new(BlockKind::Lava, Rgb::new(255, 65, 0)) } else if basalt > 0.0 diff --git a/world/src/util/structure.rs b/world/src/util/structure.rs index ed0fe38ebf..5326e359f1 100644 --- a/world/src/util/structure.rs +++ b/world/src/util/structure.rs @@ -111,6 +111,58 @@ impl StructureGen2d { ) }) } + + /// Note: Generates all possible closest samples for elements in the range + /// of min to max, *exclusive.* + pub fn iter_with_wpos( + &self, + min: Vec2, + max: Vec2, + ) -> impl Iterator, StructureField)> { + let freq = self.freq; + let spread = self.spread; + let spread_mul = Self::spread_mul(spread); + assert!(spread * 2 == spread_mul); + assert!(spread_mul <= freq); + let spread = spread as i32; + let freq = freq as i32; + let freq_offset = Self::freq_offset(freq); + assert!(freq_offset * 2 == freq); + + let min_index = Self::sample_to_index_internal(freq, min) - 1; + let max_index = Self::sample_to_index_internal(freq, max) + 1; + assert!(min_index.x < max_index.x); + // NOTE: xlen > 0 + let xlen = (max_index.x - min_index.x) as u32; + assert!(min_index.y < max_index.y); + // NOTE: ylen > 0 + let ylen = (max_index.y - min_index.y) as u32; + // NOTE: Cannot fail, since every product of u32s fits in a u64. + let len = ylen as u64 * xlen as u64; + // NOTE: since iteration is *exclusive* for the initial range, it's fine that we + // don't go up to the maximum value. + // NOTE: we convert to usize first, and then iterate, because we want to make + // sure we get a properly indexed parallel iterator that can deal with + // the whole range at once. + let x_field = self.x_field; + let y_field = self.y_field; + let seed_field = self.seed_field; + (0..len).map(move |xy| { + let index = min_index + Vec2::new((xy % xlen as u64) as i32, (xy / xlen as u64) as i32); + let index_wpos = min + Vec2::new((xy % xlen as u64) as i32, (xy / len as u64) as i32); + let field = Self::index_to_sample_internal( + freq, + freq_offset, + spread, + spread_mul, + x_field, + y_field, + seed_field, + index, + ); + (index_wpos, field) + }) + } } impl Sampler<'static> for StructureGen2d {