mirror of
https://github.com/Cactus-minecraft-server/World.git
synced 2025-12-07 10:40:37 +00:00
refactor everyhing, fixes some bugs, add a function to generate a chunk and to visualize it
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ Cargo.lock
|
|||||||
# the images
|
# the images
|
||||||
*.png
|
*.png
|
||||||
*.jpg
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
|||||||
420
src/lib.rs
420
src/lib.rs
@@ -1,418 +1,2 @@
|
|||||||
use rand::{Rng, SeedableRng};
|
mod perlin;
|
||||||
use rand_chacha::ChaCha8Rng;
|
mod superflat;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
|
|
||||||
pub struct Noise {
|
|
||||||
scale: f32,
|
|
||||||
amplitude: f32,
|
|
||||||
seed: u64,
|
|
||||||
}
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
||||||
pub struct Vector {
|
|
||||||
x: f32,
|
|
||||||
y: f32,
|
|
||||||
}
|
|
||||||
impl Noise {
|
|
||||||
pub fn new(scale: f32, amplitude: f32, seed: u64) -> Self {
|
|
||||||
assert!(scale != 0.0, "scale must be non-zero");
|
|
||||||
Self {
|
|
||||||
scale,
|
|
||||||
amplitude,
|
|
||||||
seed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get(&self, x: f32, z: f32) -> f32 {
|
|
||||||
let xs = x / self.scale;
|
|
||||||
let zs = z / self.scale;
|
|
||||||
self.perlin(xs, zs) * self.amplitude
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perlin(&self, x: f32, y: f32) -> f32 {
|
|
||||||
// 1) cellule et coords locales
|
|
||||||
let xi = x.floor() as i32;
|
|
||||||
let yi = y.floor() as i32;
|
|
||||||
let xf = x - xi as f32; // ∈ [0,1)
|
|
||||||
let yf = y - yi as f32; // ∈ [0,1)
|
|
||||||
|
|
||||||
// 2) gradients aux 4 coins (via tes helpers)
|
|
||||||
let g00 = gradient_at(xi, yi, self.seed);
|
|
||||||
let g10 = gradient_at(xi + 1, yi, self.seed);
|
|
||||||
let g01 = gradient_at(xi, yi + 1, self.seed);
|
|
||||||
let g11 = gradient_at(xi + 1, yi + 1, self.seed);
|
|
||||||
|
|
||||||
// 3) vecteurs locaux depuis chaque coin
|
|
||||||
let v00 = Vector { x: xf, y: yf };
|
|
||||||
let v10 = Vector { x: xf - 1.0, y: yf };
|
|
||||||
let v01 = Vector { x: xf, y: yf - 1.0 };
|
|
||||||
let v11 = Vector {
|
|
||||||
x: xf - 1.0,
|
|
||||||
y: yf - 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4) produits scalaires
|
|
||||||
let n00 = dot_product(&g00, &v00);
|
|
||||||
let n10 = dot_product(&g10, &v10);
|
|
||||||
let n01 = dot_product(&g01, &v01);
|
|
||||||
let n11 = dot_product(&g11, &v11);
|
|
||||||
|
|
||||||
// 5) interpolation quintique
|
|
||||||
let u = fade(xf);
|
|
||||||
let v = fade(yf);
|
|
||||||
|
|
||||||
let nx0 = linear_interpolation(n00, n10, u); // sur x, bas
|
|
||||||
let nx1 = linear_interpolation(n01, n11, u); // sur x, haut
|
|
||||||
linear_interpolation(nx0, nx1, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash2(ix: i32, iy: i32, seed: u64) -> u64 {
|
|
||||||
let mut h = seed
|
|
||||||
^ (ix as u64).wrapping_mul(0x9E3779B97F4A7C15)
|
|
||||||
^ (iy as u64).wrapping_mul(0xC2B2AE3D27D4EB4F);
|
|
||||||
h ^= h >> 33;
|
|
||||||
h = h.wrapping_mul(0xFF51AFD7ED558CCD);
|
|
||||||
h ^= h >> 33;
|
|
||||||
h = h.wrapping_mul(0xC4CEB9FE1A85EC53);
|
|
||||||
h ^= h >> 33;
|
|
||||||
h
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gradient unitaire au coin (réutilise `normalize`)
|
|
||||||
fn gradient_at(ix: i32, iy: i32, seed: u64) -> Vector {
|
|
||||||
let h = hash2(ix, iy, seed);
|
|
||||||
let mut rng = ChaCha8Rng::seed_from_u64(h);
|
|
||||||
// angle uniforme → vecteur unitaire; pas besoin d’appeler normalize ensuite
|
|
||||||
let angle: f32 = rng.gen_range(0.0..std::f32::consts::TAU);
|
|
||||||
Vector {
|
|
||||||
x: angle.cos(),
|
|
||||||
y: angle.sin(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(unused)]
|
|
||||||
fn dot_product(v1: &Vector, v2: &Vector) -> f32 {
|
|
||||||
v1.x * v2.x + v1.y * v2.y
|
|
||||||
}
|
|
||||||
#[allow(unused)]
|
|
||||||
fn calculate_norm(v1: &Vector) -> f32 {
|
|
||||||
v1.x.hypot(v1.y)
|
|
||||||
}
|
|
||||||
#[allow(unused)]
|
|
||||||
fn normalize(v1: &Vector) -> Vector {
|
|
||||||
let n = calculate_norm(v1);
|
|
||||||
if n == 0.0 {
|
|
||||||
Vector { x: 0.0, y: 0.0 };
|
|
||||||
}
|
|
||||||
Vector {
|
|
||||||
x: v1.x / n,
|
|
||||||
y: v1.y / n,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[allow(unused)]
|
|
||||||
fn fade(x: f32) -> f32 {
|
|
||||||
// Quintic fade function used in Perlin noise.
|
|
||||||
// Maps x ∈ [0,1] to a smooth curve with zero first derivative at 0 and 1.
|
|
||||||
// Formula: 6x⁵ − 15x⁴ + 10x³. Produces smooth interpolation weights.
|
|
||||||
6_f32 * x.powi(5) - 15_f32 * x.powi(4) + 10_f32 * x.powi(3)
|
|
||||||
}
|
|
||||||
/// Linearly interpolates between two scalar values.
|
|
||||||
///
|
|
||||||
/// # Parameters
|
|
||||||
/// - `a`: start value (when `t = 0.0`)
|
|
||||||
/// - `b`: end value (when `t = 1.0`)
|
|
||||||
/// - `t`: interpolation factor in `[0.0, 1.0]`
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A value on the line from `a` to `b`:
|
|
||||||
/// - if `t = 0.0`, yields `a`
|
|
||||||
/// - if `t = 1.0`, yields `b`
|
|
||||||
/// - otherwise `a + t * (b - a)`
|
|
||||||
#[allow(unused)]
|
|
||||||
fn linear_interpolation(a: f32, b: f32, t: f32) -> f32 {
|
|
||||||
a + t * (b - a)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn identify_cell(x: f32, y: f32) -> (i32, i32) {
|
|
||||||
// Use this function to identify the cell of the point that we're trying to calculate
|
|
||||||
(x.floor() as i32, y.floor() as i32)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_random_float_number(seed: i64) -> f32 {
|
|
||||||
let mut rng = ChaCha8Rng::seed_from_u64(seed as u64);
|
|
||||||
{
|
|
||||||
let this = &mut rng;
|
|
||||||
let range = -1.0..1.0;
|
|
||||||
this.random_range(range)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn get_gradient(seed: i64) -> Vector {
|
|
||||||
normalize(&Vector {
|
|
||||||
x: get_random_float_number(seed),
|
|
||||||
y: get_random_float_number(seed + 1), // éviter x == y
|
|
||||||
})
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
|
|
||||||
mod tests {
|
|
||||||
fn deriv(f: fn(f32) -> f32, x: f32) -> f32 {
|
|
||||||
let h = 1e-3;
|
|
||||||
(f(x + h) - f(x - h)) / (2.0 * h)
|
|
||||||
}
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
|
|
||||||
(a - b).abs() <= eps
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_dot_product() {
|
|
||||||
assert!(approx_eq(
|
|
||||||
dot_product(&Vector { x: 1.0, y: 0.0 }, &Vector { x: 0.0, y: 1.0 }),
|
|
||||||
0.0,
|
|
||||||
1e-6
|
|
||||||
));
|
|
||||||
assert!(approx_eq(
|
|
||||||
dot_product(&Vector { x: 1.0, y: 0.5 }, &Vector { x: 0.2, y: 1.0 }),
|
|
||||||
0.7,
|
|
||||||
1e-6
|
|
||||||
));
|
|
||||||
assert!(approx_eq(
|
|
||||||
dot_product(&Vector { x: 1.0, y: 0.5 }, &Vector { x: -0.2, y: -1.0 }),
|
|
||||||
-0.7,
|
|
||||||
1e-6
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_calculate_norm() {
|
|
||||||
assert!(approx_eq(
|
|
||||||
calculate_norm(&Vector { x: 0.5, y: 0.5 }),
|
|
||||||
0.5_f32.sqrt(),
|
|
||||||
1e-6
|
|
||||||
));
|
|
||||||
assert!(approx_eq(
|
|
||||||
calculate_norm(&Vector { x: 0.7, y: 0.3 }),
|
|
||||||
0.58_f32.sqrt(),
|
|
||||||
1e-6
|
|
||||||
));
|
|
||||||
assert!(approx_eq(
|
|
||||||
calculate_norm(&Vector { x: -0.7, y: -0.3 }),
|
|
||||||
calculate_norm(&Vector { x: 0.7, y: 0.3 }),
|
|
||||||
1e-6
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_normalize() {
|
|
||||||
let v1 = Vector { x: 0.5, y: 0.5 };
|
|
||||||
assert!(approx_eq(calculate_norm(&normalize(&v1)), 1.0, 1e-6));
|
|
||||||
let z = Vector { x: 0.0, y: 0.0 };
|
|
||||||
assert_eq!(normalize(&z), z);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_linear_interpolation() {
|
|
||||||
assert!(approx_eq(linear_interpolation(0.0, 10.0, 0.25), 2.5, 1e-6));
|
|
||||||
assert!(approx_eq(linear_interpolation(5.0, 15.0, 0.75), 12.5, 1e-6));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn dot_product_properties() {
|
|
||||||
let a = Vector { x: 0.3, y: -0.7 };
|
|
||||||
let b = Vector { x: -0.2, y: 1.1 };
|
|
||||||
assert!((dot_product(&a, &b) - dot_product(&b, &a)).abs() < 1e-6);
|
|
||||||
|
|
||||||
let k = 2.5;
|
|
||||||
let kb = Vector {
|
|
||||||
x: k * b.x,
|
|
||||||
y: k * b.y,
|
|
||||||
};
|
|
||||||
assert!((dot_product(&a, &kb) - k * dot_product(&a, &b)).abs() < 1e-6);
|
|
||||||
|
|
||||||
let p = Vector { x: 2.0, y: 4.0 };
|
|
||||||
assert!(dot_product(&p, &p) >= 0.0);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn norm_properties() {
|
|
||||||
let a = Vector { x: -0.7, y: 0.3 };
|
|
||||||
let b = Vector { x: 0.4, y: -1.2 };
|
|
||||||
assert!(
|
|
||||||
(calculate_norm(&a)
|
|
||||||
- calculate_norm(&Vector {
|
|
||||||
x: a.x.abs(),
|
|
||||||
y: a.y.abs()
|
|
||||||
}))
|
|
||||||
.abs()
|
|
||||||
< 1e-6
|
|
||||||
);
|
|
||||||
|
|
||||||
let dab = dot_product(&a, &b).abs();
|
|
||||||
assert!(dab <= calculate_norm(&a) * calculate_norm(&b) + 1e-6);
|
|
||||||
|
|
||||||
let a_plus_b = Vector {
|
|
||||||
x: a.x + b.x,
|
|
||||||
y: a.y + b.y,
|
|
||||||
};
|
|
||||||
assert!(calculate_norm(&a_plus_b) <= calculate_norm(&a) + calculate_norm(&b) + 1e-6);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn normalize_properties() {
|
|
||||||
let v = Vector { x: -0.5, y: 0.25 };
|
|
||||||
let u = normalize(&v);
|
|
||||||
assert!((calculate_norm(&u) - 1.0).abs() < 1e-6);
|
|
||||||
|
|
||||||
let uu = normalize(&u);
|
|
||||||
assert!((uu.x - u.x).abs() < 1e-6 && (uu.y - u.y).abs() < 1e-6);
|
|
||||||
|
|
||||||
if calculate_norm(&v) > 0.0 {
|
|
||||||
let cross = v.x * u.y - v.y * u.x;
|
|
||||||
assert!(cross.abs() < 1e-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
let z = Vector { x: 0.0, y: 0.0 };
|
|
||||||
assert_eq!(normalize(&z), z);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn lerp_properties() {
|
|
||||||
assert!((linear_interpolation(2.0, 8.0, 0.0) - 2.0).abs() < 1e-6);
|
|
||||||
assert!((linear_interpolation(2.0, 8.0, 1.0) - 8.0).abs() < 1e-6);
|
|
||||||
assert!((linear_interpolation(0.0, 10.0, -0.5) + 5.0).abs() < 1e-6);
|
|
||||||
|
|
||||||
let (a, b, t) = (3.0f32, 7.0f32, 0.3f32);
|
|
||||||
assert!((linear_interpolation(a, b, t) - (a + t * (b - a))).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fade_constraints() {
|
|
||||||
assert!((fade(0.0) - 0.0).abs() < 1e-6);
|
|
||||||
assert!((fade(1.0) - 1.0).abs() < 1e-6);
|
|
||||||
assert!(deriv(fade, 0.0).abs() < 1e-2);
|
|
||||||
assert!(deriv(fade, 1.0).abs() < 1e-2);
|
|
||||||
|
|
||||||
let mut prev = fade(0.0);
|
|
||||||
for i in 1..=100 {
|
|
||||||
let x = i as f32 / 100.0;
|
|
||||||
let y = fade(x);
|
|
||||||
assert!(y >= prev - 1e-6);
|
|
||||||
prev = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn perlin_is_deterministic() {
|
|
||||||
let n = Noise::new(8.0, 1.0, 42);
|
|
||||||
let a = n.get(12.345, -6.789);
|
|
||||||
let b = n.get(12.345, -6.789);
|
|
||||||
assert!((a - b).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn perlin_bounds_reasonable() {
|
|
||||||
let n = Noise::new(4.0, 1.0, 7);
|
|
||||||
for i in -5..=5 {
|
|
||||||
for j in -5..=5 {
|
|
||||||
let v = n.get(i as f32 * 0.37, j as f32 * 0.41);
|
|
||||||
assert!(v.is_finite());
|
|
||||||
assert!(v >= -1.5 && v <= 1.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn perlin_continuity_c1_local() {
|
|
||||||
let n = Noise::new(5.0, 1.0, 1);
|
|
||||||
let x = 2.3f32;
|
|
||||||
let y = -1.7f32;
|
|
||||||
let h = 1e-3f32;
|
|
||||||
let c0 = n.get(x, y);
|
|
||||||
let cx = n.get(x + h, y);
|
|
||||||
let cy = n.get(x, y + h);
|
|
||||||
assert!((cx - c0).abs() < 0.1 && (cy - c0).abs() < 0.1);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn test_perlin() -> () {
|
|
||||||
let n = Noise::new(8.0, 1.0, 42);
|
|
||||||
let h = 1e-3;
|
|
||||||
let p = n.get(3.2, 4.7);
|
|
||||||
assert!((n.get(3.2 + h, 4.7) - p).abs() < 0.1);
|
|
||||||
assert!((n.get(3.2, 4.7 + h) - p).abs() < 0.1);
|
|
||||||
assert!((n.get(4.0 - h, 4.0) - n.get(4.0 + h, 4.0)).abs() < 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The following code is GPT-5 generated and allows you to take a look at the noise
|
|
||||||
#[cfg(test)]
|
|
||||||
mod perlin_tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn perlin_deterministic() {
|
|
||||||
let n = Noise::new(32.0, 1.0, 123);
|
|
||||||
let a = n.get(12.34, -5.67);
|
|
||||||
let b = n.get(12.34, -5.67);
|
|
||||||
assert!((a - b).abs() < 1e-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn perlin_reasonable_bounds() {
|
|
||||||
let n = Noise::new(16.0, 1.0, 7);
|
|
||||||
for i in -4..=4 {
|
|
||||||
for j in -4..=4 {
|
|
||||||
let v = n.get(i as f32 * 0.37, j as f32 * 0.41);
|
|
||||||
assert!(v.is_finite());
|
|
||||||
assert!(v >= -1.5 && v <= 1.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn perlin_local_continuity() {
|
|
||||||
let n = Noise::new(24.0, 1.0, 42);
|
|
||||||
let (x, y) = (3.2f32, -4.7f32);
|
|
||||||
let h = 1e-3f32;
|
|
||||||
let c0 = n.get(x, y);
|
|
||||||
assert!((n.get(x + h, y) - c0).abs() < 0.1);
|
|
||||||
assert!((n.get(x, y + h) - c0).abs() < 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
|
||||||
mod viz {
|
|
||||||
use super::*;
|
|
||||||
use image::{GrayImage, Luma};
|
|
||||||
|
|
||||||
// try it with `cargo test -- --ignored`
|
|
||||||
#[test]
|
|
||||||
#[ignore]
|
|
||||||
fn dump_perlin_png() {
|
|
||||||
let noise = Noise::new(64.0, 10.0, 42);
|
|
||||||
|
|
||||||
let (w, h) = (512u32, 512u32);
|
|
||||||
let mut img: GrayImage = GrayImage::new(w, h);
|
|
||||||
|
|
||||||
// Simple Perlin. Pour un rendu plus riche, active la section "octaves" plus bas.
|
|
||||||
for y in 0..h {
|
|
||||||
for x in 0..w {
|
|
||||||
let fx = x as f32;
|
|
||||||
let fy = y as f32;
|
|
||||||
|
|
||||||
// --- Perlin simple ---
|
|
||||||
let mut v = noise.get(fx, fy);
|
|
||||||
|
|
||||||
let mut v = 0.0f32;
|
|
||||||
let mut amp = 1.0f32;
|
|
||||||
let mut freq = 1.0f32;
|
|
||||||
for _ in 0..5 {
|
|
||||||
v += amp * noise.perlin(fx / noise.scale * freq, fy / noise.scale * freq);
|
|
||||||
amp *= 0.5; // persistance
|
|
||||||
freq *= 2.0; // lacunarité
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalisation grossière vers [0,1] puis 8 bits
|
|
||||||
let n01 = (v * 0.5 + 0.5).clamp(0.0, 1.0);
|
|
||||||
let p = (n01 * 255.0).round() as u8;
|
|
||||||
|
|
||||||
img.put_pixel(x, y, Luma([p]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::fs::create_dir_all("target").ok();
|
|
||||||
img.save("perlin.png").expect("save png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
490
src/perlin.rs
Normal file
490
src/perlin.rs
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
use rand::{
|
||||||
|
SeedableRng,
|
||||||
|
distr::{Distribution, Uniform},
|
||||||
|
};
|
||||||
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
|
||||||
|
const CHUNK_SIZE: usize = 16;
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Noise {
|
||||||
|
// World-to-lattice scaling factor. Larger => smoother/lower-frequency noise.
|
||||||
|
scale: f32,
|
||||||
|
// Output amplitude (scalar multiplier on the Perlin value).
|
||||||
|
amplitude: f32,
|
||||||
|
// Global seed for determinism across calls.
|
||||||
|
seed: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Vector {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_height_chunk(noise: &Noise, cx: i32, cz: i32) -> [[f32; CHUNK_SIZE]; CHUNK_SIZE] {
|
||||||
|
let mut h = [[0.0; CHUNK_SIZE]; CHUNK_SIZE];
|
||||||
|
for lx in 0..CHUNK_SIZE {
|
||||||
|
for lz in 0..CHUNK_SIZE {
|
||||||
|
let wx = (cx * CHUNK_SIZE as i32 + lx as i32) as f32;
|
||||||
|
let wz = (cz * CHUNK_SIZE as i32 + lz as i32) as f32;
|
||||||
|
h[lx][lz] = noise.get(wx, wz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h
|
||||||
|
}
|
||||||
|
impl Noise {
|
||||||
|
/// Construct a noise generator.
|
||||||
|
/// `scale` must be non-zero (it rescales inputs onto the integer grid).
|
||||||
|
pub fn new(scale: f32, amplitude: f32, seed: u64) -> Self {
|
||||||
|
assert!(scale != 0.0, "scale must be non-zero");
|
||||||
|
Self {
|
||||||
|
scale,
|
||||||
|
amplitude,
|
||||||
|
seed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Query noise at world coordinates (x, z).
|
||||||
|
/// Internally rescales by `self.scale` and returns an amplitude-scaled value.
|
||||||
|
pub fn get(&self, x: f32, z: f32) -> f32 {
|
||||||
|
let xs = x / self.scale;
|
||||||
|
let zs = z / self.scale;
|
||||||
|
self.perlin(xs, zs) * self.amplitude
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standard 2D Perlin noise:
|
||||||
|
/// - Pick a unit gradient at each lattice corner via a hash.
|
||||||
|
/// - Dot product with displacement vectors from corners to point.
|
||||||
|
/// - Smoothstep (quintic) fade on each axis.
|
||||||
|
/// - Bilinear interpolation using the fade weights.
|
||||||
|
fn perlin(&self, x: f32, y: f32) -> f32 {
|
||||||
|
// Integer lattice cell containing (x, y)
|
||||||
|
let xi = x.floor() as i32;
|
||||||
|
let yi = y.floor() as i32;
|
||||||
|
|
||||||
|
// Local coordinates within the cell in [0, 1)
|
||||||
|
let xf = x - xi as f32;
|
||||||
|
let yf = y - yi as f32;
|
||||||
|
|
||||||
|
// Corner gradients (deterministic from lattice coords + seed)
|
||||||
|
let g00 = gradient_at(xi, yi, self.seed);
|
||||||
|
let g10 = gradient_at(xi + 1, yi, self.seed);
|
||||||
|
let g01 = gradient_at(xi, yi + 1, self.seed);
|
||||||
|
let g11 = gradient_at(xi + 1, yi + 1, self.seed);
|
||||||
|
|
||||||
|
// Displacement vectors from corners to (x, y)
|
||||||
|
let v00 = Vector { x: xf, y: yf };
|
||||||
|
let v10 = Vector { x: xf - 1.0, y: yf };
|
||||||
|
let v01 = Vector { x: xf, y: yf - 1.0 };
|
||||||
|
let v11 = Vector {
|
||||||
|
x: xf - 1.0,
|
||||||
|
y: yf - 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Corner contributions (projection of displacement onto gradient)
|
||||||
|
let n00 = dot_product(&g00, &v00);
|
||||||
|
let n10 = dot_product(&g10, &v10);
|
||||||
|
let n01 = dot_product(&g01, &v01);
|
||||||
|
let n11 = dot_product(&g11, &v11);
|
||||||
|
|
||||||
|
// Smooth weights along each axis
|
||||||
|
let u = fade(xf);
|
||||||
|
let v = fade(yf);
|
||||||
|
|
||||||
|
// Interpolate along x on bottom and top edges, then along y
|
||||||
|
let nx0 = linear_interpolation(n00, n10, u); // bottom edge
|
||||||
|
let nx1 = linear_interpolation(n01, n11, u); // top edge
|
||||||
|
linear_interpolation(nx0, nx1, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 2D integer hash to 64-bit state, mixed with a global seed.
|
||||||
|
/// Produces well-distributed pseudorandom bits for gradient selection.
|
||||||
|
fn hash2(ix: i32, iy: i32, seed: u64) -> u64 {
|
||||||
|
let mut h = seed
|
||||||
|
^ (ix as u64).wrapping_mul(0x9E3779B97F4A7C15)
|
||||||
|
^ (iy as u64).wrapping_mul(0xC2B2AE3D27D4EB4F);
|
||||||
|
h ^= h >> 33;
|
||||||
|
h = h.wrapping_mul(0xFF51AFD7ED558CCD);
|
||||||
|
h ^= h >> 33;
|
||||||
|
h = h.wrapping_mul(0xC4CEB9FE1A85EC53);
|
||||||
|
h ^= h >> 33;
|
||||||
|
h
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deterministic unit gradient at lattice point (ix, iy) from `seed`.
|
||||||
|
fn gradient_at(ix: i32, iy: i32, seed: u64) -> Vector {
|
||||||
|
let h = hash2(ix, iy, seed);
|
||||||
|
let mut rng = ChaCha8Rng::seed_from_u64(h);
|
||||||
|
|
||||||
|
// [0, 2π]
|
||||||
|
let dist = Uniform::new(0.0f32, std::f32::consts::TAU).unwrap();
|
||||||
|
let angle: f32 = dist.sample(&mut rng);
|
||||||
|
|
||||||
|
Vector {
|
||||||
|
x: angle.cos(),
|
||||||
|
y: angle.sin(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dot product of two 2D vectors.
|
||||||
|
fn dot_product(v1: &Vector, v2: &Vector) -> f32 {
|
||||||
|
v1.x * v2.x + v1.y * v2.y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Euclidean norm (length) of a 2D vector.
|
||||||
|
fn calculate_norm(v1: &Vector) -> f32 {
|
||||||
|
v1.x.hypot(v1.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Safe normalization to unit length.
|
||||||
|
/// Returns the zero vector unchanged to avoid division by zero / NaNs.
|
||||||
|
fn normalize(v1: &Vector) -> Vector {
|
||||||
|
let n = calculate_norm(v1);
|
||||||
|
if n == 0.0 {
|
||||||
|
return Vector { x: 0.0, y: 0.0 };
|
||||||
|
}
|
||||||
|
Vector {
|
||||||
|
x: v1.x / n,
|
||||||
|
y: v1.y / n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quintic fade function used by Perlin noise.
|
||||||
|
/// Maps x ∈ [0,1] to a smooth S-curve with zero first derivative at 0 and 1.
|
||||||
|
/// 6x⁵ − 15x⁴ + 10x³
|
||||||
|
fn fade(x: f32) -> f32 {
|
||||||
|
6_f32 * x.powi(5) - 15_f32 * x.powi(4) + 10_f32 * x.powi(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linear interpolation between scalars `a` and `b` by factor `t`.
|
||||||
|
fn linear_interpolation(a: f32, b: f32, t: f32) -> f32 {
|
||||||
|
a + t * (b - a)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
fn deriv(f: fn(f32) -> f32, x: f32) -> f32 {
|
||||||
|
let h = 1e-3;
|
||||||
|
(f(x + h) - f(x - h)) / (2.0 * h)
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
|
||||||
|
(a - b).abs() <= eps
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_product() {
|
||||||
|
assert!(approx_eq(
|
||||||
|
dot_product(&Vector { x: 1.0, y: 0.0 }, &Vector { x: 0.0, y: 1.0 }),
|
||||||
|
0.0,
|
||||||
|
1e-6
|
||||||
|
));
|
||||||
|
assert!(approx_eq(
|
||||||
|
dot_product(&Vector { x: 1.0, y: 0.5 }, &Vector { x: 0.2, y: 1.0 }),
|
||||||
|
0.7,
|
||||||
|
1e-6
|
||||||
|
));
|
||||||
|
assert!(approx_eq(
|
||||||
|
dot_product(&Vector { x: 1.0, y: 0.5 }, &Vector { x: -0.2, y: -1.0 }),
|
||||||
|
-0.7,
|
||||||
|
1e-6
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_calculate_norm() {
|
||||||
|
assert!(approx_eq(
|
||||||
|
calculate_norm(&Vector { x: 0.5, y: 0.5 }),
|
||||||
|
0.5_f32.sqrt(),
|
||||||
|
1e-6
|
||||||
|
));
|
||||||
|
assert!(approx_eq(
|
||||||
|
calculate_norm(&Vector { x: 0.7, y: 0.3 }),
|
||||||
|
0.58_f32.sqrt(),
|
||||||
|
1e-6
|
||||||
|
));
|
||||||
|
assert!(approx_eq(
|
||||||
|
calculate_norm(&Vector { x: -0.7, y: -0.3 }),
|
||||||
|
calculate_norm(&Vector { x: 0.7, y: 0.3 }),
|
||||||
|
1e-6
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_normalize() {
|
||||||
|
let v1 = Vector { x: 0.5, y: 0.5 };
|
||||||
|
assert!(approx_eq(calculate_norm(&normalize(&v1)), 1.0, 1e-6));
|
||||||
|
let z = Vector { x: 0.0, y: 0.0 };
|
||||||
|
assert_eq!(normalize(&z), z);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_linear_interpolation() {
|
||||||
|
assert!(approx_eq(linear_interpolation(0.0, 10.0, 0.25), 2.5, 1e-6));
|
||||||
|
assert!(approx_eq(linear_interpolation(5.0, 15.0, 0.75), 12.5, 1e-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dot_product_properties() {
|
||||||
|
let a = Vector { x: 0.3, y: -0.7 };
|
||||||
|
let b = Vector { x: -0.2, y: 1.1 };
|
||||||
|
assert!((dot_product(&a, &b) - dot_product(&b, &a)).abs() < 1e-6);
|
||||||
|
|
||||||
|
let k = 2.5;
|
||||||
|
let kb = Vector {
|
||||||
|
x: k * b.x,
|
||||||
|
y: k * b.y,
|
||||||
|
};
|
||||||
|
assert!((dot_product(&a, &kb) - k * dot_product(&a, &b)).abs() < 1e-6);
|
||||||
|
|
||||||
|
let p = Vector { x: 2.0, y: 4.0 };
|
||||||
|
assert!(dot_product(&p, &p) >= 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn norm_properties() {
|
||||||
|
let a = Vector { x: -0.7, y: 0.3 };
|
||||||
|
let b = Vector { x: 0.4, y: -1.2 };
|
||||||
|
assert!(
|
||||||
|
(calculate_norm(&a)
|
||||||
|
- calculate_norm(&Vector {
|
||||||
|
x: a.x.abs(),
|
||||||
|
y: a.y.abs()
|
||||||
|
}))
|
||||||
|
.abs()
|
||||||
|
< 1e-6
|
||||||
|
);
|
||||||
|
|
||||||
|
let dab = dot_product(&a, &b).abs();
|
||||||
|
assert!(dab <= calculate_norm(&a) * calculate_norm(&b) + 1e-6);
|
||||||
|
|
||||||
|
let a_plus_b = Vector {
|
||||||
|
x: a.x + b.x,
|
||||||
|
y: a.y + b.y,
|
||||||
|
};
|
||||||
|
assert!(calculate_norm(&a_plus_b) <= calculate_norm(&a) + calculate_norm(&b) + 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalize_properties() {
|
||||||
|
let v = Vector { x: -0.5, y: 0.25 };
|
||||||
|
let u = normalize(&v);
|
||||||
|
assert!((calculate_norm(&u) - 1.0).abs() < 1e-6);
|
||||||
|
|
||||||
|
let uu = normalize(&u);
|
||||||
|
assert!((uu.x - u.x).abs() < 1e-6 && (uu.y - u.y).abs() < 1e-6);
|
||||||
|
|
||||||
|
if calculate_norm(&v) > 0.0 {
|
||||||
|
let cross = v.x * u.y - v.y * u.x;
|
||||||
|
assert!(cross.abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
let z = Vector { x: 0.0, y: 0.0 };
|
||||||
|
assert_eq!(normalize(&z), z);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lerp_properties() {
|
||||||
|
assert!((linear_interpolation(2.0, 8.0, 0.0) - 2.0).abs() < 1e-6);
|
||||||
|
assert!((linear_interpolation(2.0, 8.0, 1.0) - 8.0).abs() < 1e-6);
|
||||||
|
assert!((linear_interpolation(0.0, 10.0, -0.5) + 5.0).abs() < 1e-6);
|
||||||
|
|
||||||
|
let (a, b, t) = (3.0f32, 7.0f32, 0.3f32);
|
||||||
|
assert!((linear_interpolation(a, b, t) - (a + t * (b - a))).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fade_constraints() {
|
||||||
|
assert!((fade(0.0) - 0.0).abs() < 1e-6);
|
||||||
|
assert!((fade(1.0) - 1.0).abs() < 1e-6);
|
||||||
|
assert!(deriv(fade, 0.0).abs() < 1e-2);
|
||||||
|
assert!(deriv(fade, 1.0).abs() < 1e-2);
|
||||||
|
|
||||||
|
let mut prev = fade(0.0);
|
||||||
|
for i in 1..=100 {
|
||||||
|
let x = i as f32 / 100.0;
|
||||||
|
let y = fade(x);
|
||||||
|
assert!(y >= prev - 1e-6);
|
||||||
|
prev = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perlin_is_deterministic() {
|
||||||
|
let n = Noise::new(8.0, 1.0, 42);
|
||||||
|
let a = n.get(12.345, -6.789);
|
||||||
|
let b = n.get(12.345, -6.789);
|
||||||
|
assert!((a - b).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perlin_bounds_reasonable() {
|
||||||
|
let n = Noise::new(4.0, 1.0, 7);
|
||||||
|
for i in -5..=5 {
|
||||||
|
for j in -5..=5 {
|
||||||
|
let v = n.get(i as f32 * 0.37, j as f32 * 0.41);
|
||||||
|
assert!(v.is_finite());
|
||||||
|
assert!(v >= -1.5 && v <= 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perlin_continuity_c1_local() {
|
||||||
|
let n = Noise::new(5.0, 1.0, 1);
|
||||||
|
let x = 2.3f32;
|
||||||
|
let y = -1.7f32;
|
||||||
|
let h = 1e-3f32;
|
||||||
|
let c0 = n.get(x, y);
|
||||||
|
let cx = n.get(x + h, y);
|
||||||
|
let cy = n.get(x, y + h);
|
||||||
|
assert!((cx - c0).abs() < 0.1 && (cy - c0).abs() < 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_perlin() {
|
||||||
|
let n = Noise::new(8.0, 1.0, 42);
|
||||||
|
let h = 1e-3;
|
||||||
|
let p = n.get(3.2, 4.7);
|
||||||
|
assert!((n.get(3.2 + h, 4.7) - p).abs() < 0.1);
|
||||||
|
assert!((n.get(3.2, 4.7 + h) - p).abs() < 0.1);
|
||||||
|
assert!((n.get(4.0 - h, 4.0) - n.get(4.0 + h, 4.0)).abs() < 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod perlin_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perlin_deterministic() {
|
||||||
|
let n = Noise::new(32.0, 1.0, 123);
|
||||||
|
let a = n.get(12.34, -5.67);
|
||||||
|
let b = n.get(12.34, -5.67);
|
||||||
|
assert!((a - b).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perlin_reasonable_bounds() {
|
||||||
|
let n = Noise::new(16.0, 1.0, 7);
|
||||||
|
for i in -4..=4 {
|
||||||
|
for j in -4..=4 {
|
||||||
|
let v = n.get(i as f32 * 0.37, j as f32 * 0.41);
|
||||||
|
assert!(v.is_finite());
|
||||||
|
assert!(v >= -1.5 && v <= 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn perlin_local_continuity() {
|
||||||
|
let n = Noise::new(24.0, 1.0, 42);
|
||||||
|
let (x, y) = (3.2f32, -4.7f32);
|
||||||
|
let h = 1e-3f32;
|
||||||
|
let c0 = n.get(x, y);
|
||||||
|
assert!((n.get(x + h, y) - c0).abs() < 0.1);
|
||||||
|
assert!((n.get(x, y + h) - c0).abs() < 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod viz {
|
||||||
|
use super::*;
|
||||||
|
use image::{GrayImage, Luma};
|
||||||
|
|
||||||
|
// Render a preview image. Run with: `cargo test -- --ignored`
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn dump_perlin_png() {
|
||||||
|
let noise = Noise::new(64.0, 10.0, 42);
|
||||||
|
|
||||||
|
let (w, h) = (512u32, 512u32);
|
||||||
|
let mut img: GrayImage = GrayImage::new(w, h);
|
||||||
|
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let fx = x as f32;
|
||||||
|
let fy = y as f32;
|
||||||
|
|
||||||
|
// Multi-octave (fractal Brownian motion) for richer structure.
|
||||||
|
let mut v = 0.0f32;
|
||||||
|
let mut amp = 1.0f32;
|
||||||
|
let mut freq = 1.0f32;
|
||||||
|
for _ in 0..5 {
|
||||||
|
v += amp * noise.perlin(fx / noise.scale * freq, fy / noise.scale * freq);
|
||||||
|
amp *= 0.5;
|
||||||
|
freq *= 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map from roughly [-1,1] to [0,255]
|
||||||
|
let n01 = (v * 0.5 + 0.5).clamp(0.0, 1.0);
|
||||||
|
let p = (n01 * 255.0).round() as u8;
|
||||||
|
|
||||||
|
img.put_pixel(x, y, Luma([p]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::create_dir_all("target").ok();
|
||||||
|
img.save("perlin.png").expect("save png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
mod viz_chunk {
|
||||||
|
use super::*;
|
||||||
|
use image::{GrayImage, Luma};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn dump_chunk_png() {
|
||||||
|
let noise = Noise::new(64.0, 10.0, 42);
|
||||||
|
let chunks_x: usize = 32;
|
||||||
|
let chunks_z: usize = 32;
|
||||||
|
|
||||||
|
let w = (chunks_x * CHUNK_SIZE) as u32;
|
||||||
|
let h = (chunks_z * CHUNK_SIZE) as u32;
|
||||||
|
|
||||||
|
let mut field = vec![0.0f32; (w * h) as usize];
|
||||||
|
let mut vmin = f32::INFINITY;
|
||||||
|
let mut vmax = f32::NEG_INFINITY;
|
||||||
|
|
||||||
|
for cz in 0..chunks_z {
|
||||||
|
for cx in 0..chunks_x {
|
||||||
|
let tile = generate_height_chunk(&noise, cx as i32, cz as i32);
|
||||||
|
|
||||||
|
for lz in 0..CHUNK_SIZE {
|
||||||
|
for lx in 0..CHUNK_SIZE {
|
||||||
|
let x = cx * CHUNK_SIZE + lx;
|
||||||
|
let z = cz * CHUNK_SIZE + lz;
|
||||||
|
let idx = z * (w as usize) + x;
|
||||||
|
|
||||||
|
let v = tile[lx][lz];
|
||||||
|
field[idx] = v;
|
||||||
|
if v < vmin {
|
||||||
|
vmin = v;
|
||||||
|
}
|
||||||
|
if v > vmax {
|
||||||
|
vmax = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut img: GrayImage = GrayImage::new(w, h);
|
||||||
|
let span = (vmax - vmin).max(std::f32::EPSILON);
|
||||||
|
for z in 0..(h as usize) {
|
||||||
|
for x in 0..(w as usize) {
|
||||||
|
let v = field[z * (w as usize) + x];
|
||||||
|
let n01 = ((v - vmin) / span).clamp(0.0, 1.0);
|
||||||
|
let p = (n01 * 255.0).round() as u8;
|
||||||
|
img.put_pixel(x as u32, z as u32, Luma([p]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::fs::create_dir_all("target").ok();
|
||||||
|
img.save("chunk_heightmap.png").expect("save png");
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/superflat.rs
Normal file
1
src/superflat.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user