-
Notifications
You must be signed in to change notification settings - Fork 20
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
Comments
|
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. |
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.
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. |
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. |
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. |
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. |
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. |
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. ;) |
I'll definitely take a stab at this as time allows, it would make my life easier too. Also, PRs are welcome! |
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 |
As of #27 the fundamental primitives have well-defined ranges, but the higher-level interfaces like |
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 Is there any chance someone with more knowledge could revisit this issue and provide |
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. |
Looking at 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 So the closed-form sum would be is that correct? |
I'm pretty lost. I wrote this code to experimentally detect the range of 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 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 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 I'd really appreciate it if I could get some feedback on which assumptions I'm making are wrong. |
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. |
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 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 |
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. |
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: copying the I'm running on Windows 10, Intel Core i7 (9th gen). It's surprising to me that the 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. |
I downloaded the latest
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 |
All of the above essentially to say that having the output range precomputed and made available on the |
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. |
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 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 🤷 |
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.
The text was updated successfully, but these errors were encountered: