verpeteren / rust-simd-noise

SIMD noise library for Rust
https://crates.io/crates/simdnoise
251 stars 20 forks source link

Offset seams with high octaves and low frequency #39

Open parasyte opened 2 years ago

parasyte commented 2 years ago

I'm trying to create seamless textures using offsets, and even a naive attempt is failing when I combine a low frequency (like the default value 0.02) with a high number of octaves (like 8).

To make the issue even more obvious, I'm using a frequency of 0.125 in the images below, which gives it enough structure to verify the bug is not in the offsets I'm using or how the image is created.

Dimensionality doesn't matter. The seam appears with 2d, 3d, and 4d.

The code Change the comments on lines 48 and 49 to swap between the expected result (`get_buffer_1`) and the actual result (`get_buffer_2`). ```rust use minifb::{Key, Window, WindowOptions}; const WIDTH: usize = 2048; const HEIGHT: usize = 1024; const HWIDTH: usize = WIDTH / 2; const FREQ: f32 = 0.125; const OCTAVES: u8 = 8; #[allow(dead_code)] fn get_buffer_1() -> Vec { let noise = simdnoise::NoiseBuilder::turbulence_3d_offset(0.0, WIDTH, 0.0, HEIGHT, 0.0, 1) .with_freq(FREQ) .with_octaves(OCTAVES) .generate_scaled(0.0, 255.0); noise.iter().map(|x| *x as u32 * 0x00010101).collect() } #[allow(dead_code)] fn get_buffer_2() -> Vec { let noise = simdnoise::NoiseBuilder::turbulence_3d_offset(0.0, HWIDTH, 0.0, HEIGHT, 0.0, 1) .with_freq(FREQ) .with_octaves(OCTAVES) .generate_scaled(0.0, 255.0); let noise2 = simdnoise::NoiseBuilder::turbulence_3d_offset(HWIDTH as f32, HWIDTH, 0.0, HEIGHT, 0.0, 1) .with_freq(FREQ) .with_octaves(OCTAVES) .generate_scaled(0.0, 255.0); let mut buffer = Vec::with_capacity(WIDTH * HEIGHT); for (i, j) in noise.chunks_exact(HWIDTH).zip(noise2.chunks_exact(HWIDTH)) { for x in i { buffer.push(*x as u32 * 0x00010101); } for x in j { buffer.push(*x as u32 * 0x00010101); } } buffer } fn main() { // Use either of the buffers // let buffer = get_buffer_1(); let buffer = get_buffer_2(); let mut window = Window::new( "Test - ESC to exit", WIDTH, HEIGHT, WindowOptions::default(), ) .unwrap_or_else(|e| { panic!("{}", e); }); // Limit to max ~60 fps update rate window.limit_update_rate(Some(std::time::Duration::from_micros(16600))); while window.is_open() && !window.is_key_down(Key::Escape) { // We unwrap here as we want this code to exit if it fails. Real applications may want to handle this in a different way window.update_with_buffer(&buffer, WIDTH, HEIGHT).unwrap(); } } ```

Expected:

expected

Actual:

actual

The more I play around with various settings, the more I am convinced that there is some contribution from neighboring nodes that affects the output. For instance, if you put both images into layers and apply a difference operator, you'll see that they are roughly the same, but the left side is a bit brighter in the second image (where the seam appears) and the right side is slightly darker.

Increasing the frequency beyond 1.0 or lowering the octaves below 5 makes the seam incredibly difficult to notice. But I would be surprised if it ever goes away completely.

parasyte commented 2 years ago

The scaling function is the cause. The discussion in #23 seems to hint at that being the case.

If I replace generate_scaled() with generate and do my own scaling, I can generate seamless textures. The trouble is that I need to know the full range for every possible input to scale every possible output to [0, 255]. Given the settings I am using, the min and max are 0.0 and 255.0. Scaling with this expectation produces a result that is close to the first expected image, just a little darker (because the actual max value seen is about 202.07).

I am going to tentatively say this issue can be resolved by doing your own scaling. But since #27, the generate_scaled() methods can just use a closed form expression to compute min/max instead of iterating the samples seen. It seems like that would "do the right thing" and prevent the issue that I stumbled upon.

tricarbonate commented 1 year ago

Hey, I had a similar problem that is related to the generate_scaled() method. I was trying to generate 3d chunks using ridge_3d_offset with generate_scaled(-1.0, 1.0) and it resulted in offsets in neighbouring chunks image

Applying my own scaling did not work because the min/max values for each chunks are different, so the scaling is never the same between. The only thing that works for me now is using the generate() method, and apply an offset, without scaling.

I think it would be great to have a way to get the theoretical min/max of the noise function, so that I could apply the same scaling across chunks. Maybe I'm missing something.