Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document domain and range of primitive functions #23

Open
Ralith opened this issue Jul 7, 2020 · 23 comments
Open

Document domain and range of primitive functions #23

Ralith opened this issue Jul 7, 2020 · 23 comments

Comments

@Ralith
Copy link
Contributor

Ralith commented Jul 7, 2020

Experimentally I see that e.g. fbm_3d with parameters similar to the defaults seems to return values mostly in the range of -0.1..0.1. simplex_3d likes even smaller magnitudes. This is very confusing, as traditionally these noises are presented as having a range of 0..1 or -1..1. I'm guessing the domain is such that an interesting sample must cover multiple integers' worth of space, but even sampling quite large domains doesn't seem to get me a sensible range. The scaled high-level functions appear to naively remap the min/max that was actually generated, which doesn't seem sensible when I want to generate consistent results across multiple adjoining regions.

I'm sure I'm missing something stupid here, but if the docs spelled these things out it would save me a great deal of confusion all the same.

@Ralith
Copy link
Contributor Author

Ralith commented Jul 7, 2020

simplex::simplex_1d(0.3, 0) returns 1.4400741, which is well outside of any conventional range for simplex. Is this intended?

@jackmott
Copy link
Collaborator

jackmott commented Jul 8, 2020

So many noise libraries will do a multiply and add at the end to bring the output to some range of 0..1 or -1..1. However once you start doing FBM or similar, with various parameters, that range goes out the window anyway. So I just leave that step out, and the scaling functions are provided to scale the output to whatever range you want.

@Ralith
Copy link
Contributor Author

Ralith commented Jul 8, 2020

So many noise libraries will do a multiply and add at the end to bring the output to some range of 0..1 or -1..1. However once you start doing FBM or similar, with various parameters, that range goes out the window anyway.

How so? If you compose or transform a noise function, you're composing/transforming the ranges too, and can therefore normalize them into a well-defined range afterwards.

So I just leave that step out, and the scaling functions are provided to scale the output to whatever range you want.

These don't work. I need consistent results between multiple samplings of nearby coordinates. Taking the min/max of the returned value and scaling that range to fit will result in discontinuities, not to mention being a chunk of extra CPU work.

I'm mapping noise into a fixed-point range for compact use on the GPU, so having well defined ranges is very important to making effective use of available precision.

@jackmott
Copy link
Collaborator

jackmott commented Jul 8, 2020

Then you will need to experimentally determine, for whatever parameters you are using, the scaling and offset necessary to make the range -1..1 (or whatever you want it to be) and apply it.

@Ralith
Copy link
Contributor Author

Ralith commented Jul 8, 2020

That's frustrating, because it's difficult, unreliable, and absolutely not necessary--all these functions have analytically well-defined ranges, and it's entirely possible for the library to expose this, as most other noise libraries do.

@jackmott
Copy link
Collaborator

jackmott commented Jul 8, 2020

If you can point me to a nosie library that does this the way you expect I'll see what I can do. The ones I have seen simply determine experimentally a scale/offset that only works in the case of a single octave.

@Ralith
Copy link
Contributor Author

Ralith commented Jul 8, 2020

By other implementations, I'm referring principally to implementations of the simplex noise primitive, all of which I've reviewed provide noise in the -1..1 or 0..1 range. For example, see Simplex noise demystified.

For compositions of primitives, it's a matter of analyzing the composition. For example, FBM noise as implemented in this crate is the summation of N simplex octaves, where each octave is scaled by gain^n. Its bounds are therefore the simplex bounds with each extreme multiplied by (1 + gain + gain^2 + ... + gain^n). This can be computed cheaply in the course of the existing summation, and used to normalize the results.

@virtualritz
Copy link
Contributor

I worked as a shader writer & TD for 15 years in the VFX industry. Professional renderers normalize their noise functions (all of them) over -1..1 or 0..1. And when you add octaves it's then easy to make sure you stay within these ranges which are important for e.g. calculating displacementbound & co, indexing into a color ramp etc.

See e.g. OSL's simplex noise implementation.

Maybe "other noise libraries" are a bad reference?

I would rather look to uses of such noise libraries in professional renderers used by people paid extremely well to produce that kind of pictures you see on you favorite summer blockbuster or streaming sci-fi/fantasy series. ;)

@jackmott
Copy link
Collaborator

jackmott commented Jul 8, 2020

I'll definitely take a stab at this as time allows, it would make my life easier too. Also, PRs are welcome!

@Ralith
Copy link
Contributor Author

Ralith commented Jul 8, 2020

Awesome to hear that, thanks! I've been reviewing the literature and various reference implementations in the hopes of getting a better handle on this and #24. Stefan Gustavson's C implementations are very well-commented and readable, but regrettably the scaling factors used there don't have derivations attached and this crate's implementation seems to have diverged somehow such that they no longer match; e.g. his sdnoise1 calls for scaling by 1 / (8*(3/4)^4), which empirically produces a suspiciously round range of ±7/8 in this crate's simplex_1d impl.

@Ralith
Copy link
Contributor Author

Ralith commented Jul 18, 2020

As of #27 the fundamental primitives have well-defined ranges, but the higher-level interfaces like fbm do not. I don't presently plan to address them personally, but they should be comparatively straightforward based on the principle that d/dx (f(x) + g(x)) = d/dx f(x) + d/dx g(x), and d/dx c * f(x) = c * d/dx f(x).

@Riizade
Copy link

Riizade commented Aug 15, 2021

Hey! I'm running into this issue currently. For now, I think I'll experimentally generate a huge number of noise values with my preferred settings, then hardcode the results to avoid the runtime cost of re-calculating the bounds.

This is certainly less than ideal, but I don't have the knowledge of noise functions to understand how their parameters affect their range.

Naively, I think the gain and octaves would affect the range, but lacunarity and frequency would not, but I'm mostly taking a stab in the dark.

Is there any chance someone with more knowledge could revisit this issue and provide output_min()/output_max() functions on FbmSettings (and ideally for other applicable noise settings structs)? I could then make an attempt at altering generated_scaled() to use the global min/max instead of the local min/max, but that's so trivial as to probably not be worth a separate PR.

@Ralith @jackmott @virtualritz

@Ralith
Copy link
Contributor Author

Ralith commented Aug 15, 2021

I recommend just looking at the implementation and analyzing them to determine the necessary scaling factor. There's not all that much math going on.

@Riizade
Copy link

Riizade commented Aug 15, 2021

Looking at simplex_2d, it looks like it generates a value from -1 to 1
fbm_2d is implemented here: https://github.com/jackmott/rust-simd-noise/blob/master/src/simplex.rs#L322

and can be simplified to:

    let mut result = simplex_2d(x, y, seed); // range of -1 to 1
    let mut amp = 1.0;

    for _ in 1..octaves {
        x = x * lac;
        y = y * lac;
        amp = amp * gain;
        result = (simplex_2d(x, y, seed) * amp) + result;
    }

    result

So the maximum value is gain^0 + gain^1 + gain^2... which is a geometric series of the form n=octaves a=1 and r=gain

So the closed-form sum would be 1* ((1-gain^(octaves+1))/(1 - gain))

is that correct?

@Riizade
Copy link

Riizade commented Aug 15, 2021

I'm pretty lost.

I wrote this code to experimentally detect the range of simplex_2d which is documented as -1 <= n<= 1 (https://github.com/jackmott/rust-simd-noise/blob/master/src/simplex.rs#L209)

        let mut max: f32 = 0.0;
        for a in 0..100 {
            for b in 0..100 {
                unsafe {
                    let mut result = simplex_2d::<Scalar>(
                        F32x1((a * 10) as f32),
                        F32x1((b * 10) as f32),
                        self.height_seed,
                    )
                    .0;
                    if result.abs() > max.abs() {
                        max = result;
                    }
                }
            }
        }

I get 0.02138349 . Considering I'm only testing 10,000 locations, I don't expect it to be 1.0, but 0.02... is pretty far away, I'd expect to get higher than that.

Further, I tried to experiment with generating fractal brownian motion directly from simplex noise by copying the implementation above and came up with the following:

unsafe {
            let mut x = chunk_coord.z as f32 * CHUNK_SIZE.z as f32;
            let mut y = chunk_coord.x as f32 * CHUNK_SIZE.x as f32;
            let mut amp = 1.0;
            let mut result = simplex_2d::<Scalar>(F32x1(x), F32x1(y), self.height_seed).0;

            for _ in 1..octaves {
                x = x * lacunarity;
                y = y * lacunarity;
                amp = amp * gain;
                result =
                    result + (simplex_2d::<Scalar>(F32x1(x), F32x1(y), self.height_seed).0 * amp);
            }

            x = chunk_coord.z as f32 * CHUNK_SIZE.z as f32;
            y = chunk_coord.x as f32 * CHUNK_SIZE.x as f32;

            println!("manual result: {:?}", result);
            println!(
                "actual result: {:?}",
                NoiseBuilder::fbm_2d_offset(x, 1, y, 1)
                    .with_seed(self.height_seed)
                    .with_octaves(octaves)
                    .with_gain(gain)
                    .with_freq(frequency)
                    .with_lacunarity(lacunarity)
                    .generate()
                    .0
                    .get(0)
                    .unwrap()
            )
        }

My manual implementation and the crate's implementation differ, I do not get the same result between the two implementations.

I looked at the implementation of set1_ps, mul_ps, and add_ps in case they did something other than what I expected, but they seem to be straightforward assignment, multiplication, and addition operations as far as I can tell.

At this point I'm pretty lost on how to go about calculating the range of the outputs considering I can't even get the simplex_2d function to return its documented range. I must be doing something incredibly stupid or very subtly wrong (like, maybe the endianness of my f32s is opposite what is expected when converting them to a SIMD float?)

I'd really appreciate it if I could get some feedback on which assumptions I'm making are wrong.

@Ralith
Copy link
Contributor Author

Ralith commented Aug 15, 2021

I wrote this code to experimentally detect the range of simplex_2d

You're sampling at locations that are multiples of integers. Simplex noise is fundamentally laid out on a skewed grid, so this gives you extremely biased results. Try scaling such that your grid spacing is something like 0.01 instead of 10.

@Riizade
Copy link

Riizade commented Aug 15, 2021

Altering to

let mut max: f32 = 0.0;
for a in 0..100 {
    for b in 0..100 {
        unsafe {
            let mut result = simplex_2d::<Scalar>(
                F32x1(a as f32 * 0.013),
                F32x1(b as f32 * 1.278),
                self.height_seed,
            )
            .0;
            if result.abs() > max.abs() {
                max = result;
            }
        }
    }
}

println!("experimental simplex_2d max: {:?}", max);

gives the output experimental simplex_2d max: 0.022090733

I'm not sure I'm understanding correctly, but since I'm now providing non-integers as inputs to the function, the results shouldn't be biased in the way you suggest, right? But the results still don't indicate a likely range of 1 <= n <= 1

@Ralith
Copy link
Contributor Author

Ralith commented Aug 16, 2021

That looks more reasonable, though your y-axis scale is still way too high to be sensible. If you're sure you're still having issues after turning that down, I recommend comparing with https://github.com/jackmott/rust-simd-noise/blob/master/src/simplex.rs#L1132-L1148, which is doing more or less the same thing, and passing on master.

@Riizade
Copy link

Riizade commented Aug 16, 2021

I copied the implementation from the test and ran the following:

fn simplex_2d_range() {
    for seed in 0..10 {
        let mut min = f32::INFINITY;
        let mut max = -f32::INFINITY;
        for y in 0..10 {
            for x in 0..100 {
                let n = unsafe {
                    simplex_2d::<Scalar>(F32x1(x as f32 / 10.0), F32x1(y as f32 / 10.0), seed).0
                };
                min = min.min(n);
                max = max.max(n);
            }
        }
        println!("min: {:?} // max {:?}", min, max);
    }
}

which results in several outputs of the same magnitude, for example: min: -0.021640444 // max 0.022089224

copying the check_bounds() function and running it fails the assertion, as expected.

I'm running on Windows 10, Intel Core i7 (9th gen). It's surprising to me that the Scalar output range would differ between machines, but that looks to be what's happening.

I don't think there's a way I could be polluting the scope of the function such that it's doing something else, but open to other explanations.

I'm a little at a loss. If the output range differs between different machines, there's not a great way to scale the output without running some tests to verify the bounds first. Which isn't the worst thing in the world considering how cheap the operations are (awesome work, by the way), but less than ideal from an ergonomics perspective.

@Riizade
Copy link

Riizade commented Aug 16, 2021

I downloaded the latest master and ran cargo test, and the tests pass

running 17 tests
test simplex_64::tests::simplex_1d_range ... ok
test simplex::tests::simplex_2d_deriv_sanity ... ok
test simplex::tests::simplex_1d_range ... ok
test simplex_64::tests::simplex_2d_range ... ok
test simplex::tests::simplex_2d_range ... ok
test tests::consistency_1d ... ok
test simplex::tests::simplex_1d_deriv_sanity ... ok
test tests::small_dimensions ... ok
test simplex::tests::simplex_3d_deriv_sanity ... ok
test tests::cell_consistency_2d ... ok
test tests::consistency_4d ... ok
test tests::consistency_3d ... ok
test tests::consistency_2d ... ok
test tests::cell_consistency_3d ... ok
test simplex_64::tests::simplex_4d_range ... ok
test simplex::tests::simplex_4d_range ... ok
test simplex::tests::simplex_3d_range ... ok

so it doesn't look to be a machine difference, but something wonky happening deep in my code somewhere. It's not a lot of code, and I'm not doing anything particularly arcane, but maybe it's something with memory layout since the functions are in an unsafe block.

@Riizade
Copy link

Riizade commented Aug 16, 2021

All of the above essentially to say that having the output range precomputed and made available on the FbmSettings struct or elsewhere would be a great feature and avoid much of this hassle for the next person who comes along, but I'm not able to implement it myself.

@Ralith
Copy link
Contributor Author

Ralith commented Aug 16, 2021

If you're getting different results from identical code, then UB seems likely, yeah. If there's UB, the scale of the output is the least of your concerns. Once you've addressed that, I think you were on the right track for your analysis. I don't have time to dig deeply into this myself, but since you've got the same code failing in one case and passing in another, you should be able to isolate where they diverge.

@Inspirateur
Copy link

So I dug a bit and it seems the simplex_2d code uses this (or the same source as) https://procedural-content-generation.fandom.com/wiki/Simplex_Noise
Everything seems to match except the 70x scaling on the return value which is absent in this crate (as the author stated).

70x would scale values between -0.014 ; 0.014 to -1 ; 1, but the weird thing is that when I sample from simplex_2d the range i get is -0.021 ; 0.021 so x70 would not work here.

I don't know if the wiki or the implementation is wrong though but i empirically I guess doing x40 would more or less work 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants