-
Notifications
You must be signed in to change notification settings - Fork 14
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
Add noise map generation utilities #221
Conversation
Previously I was overloading width and height to mean two different things. The width and height are properties of the underlying noise, and not related to the actual dimensions of the noise map. Since the noise map is in theory infinite resolution, the actual width and height make no sense.
[Property(Arbitrary = new[] { typeof(DoubleGenerators.UnitIntervalUpperBoundExclusive) })] | ||
public void GeneratesMapThatThrowsIfCoordinatesTooSmall(int seed, double x, double y) | ||
{ | ||
var map = PerlinNoise.Generate(5, 5, seed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the tests here use 5x5 size, and there are no tests to validate whether these parameters do anything at all. Is that alright?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained in the PR description: the tests om this PR are by no means meant to be exhaustive.
In addition, it is hard to test that these parameters really do something. The randomness involved makes it hard to make hard assertions about the output. There is always a chance that the random output of a 10x10 grid just happens to line up really well with the properties of a 5x5 grid.
We can always generate two maps with the exact same seed with different dimensions and assume they are different in at least one point when converted to a discrete array, but there is no guarantee this is going to be the case for all seeds, and I think even that level of flakiness is not something we want to avoid in our tests.
{ | ||
var map = PerlinNoise.Generate(5, 5, seed); | ||
|
||
Func<double> action = () => map.ValueAt(x - 1, y - 1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This and the test below can't tell if both coordinates are being checked for range correctly, and the -1/+1 makes it look a lot like discrete indices, even though it isn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed by splitting the tests across dimensions and changing the 1
to 1.0
. The choice for 1 is on purpose as it will exercise edge cases.
} | ||
} | ||
|
||
public interface IInterpolationMethod1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we intend others to implement this? If so, should it be outside this class perhaps? If not, it should be an abstract class with internal protected constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It already is an outer class. I just didn't really want to have a new file for just this one line method. That being said, given the confusion this seems to be leading to, I have extracted these interfaces to separate files.
Bearded.Utilities/Noise/INoiseMap.cs
Outdated
@@ -0,0 +1,41 @@ | |||
namespace Bearded.Utilities.Noise | |||
{ | |||
public interface INoiseMap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is nothing about this interface related to actual noise. Should we rename it more general?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to ProceduralTexture
since according to Wikipedia, that is the right name for something like this. Perlin noise specifically is mentioned as an example. I'm open to renaming the entire namespace, or move the non-noise specific procedural texture types to Core
or their own namespace.
Bearded.Utilities/Noise/NoiseMap.cs
Outdated
var u = x - xBelow; | ||
var v = y - yBelow; | ||
|
||
xBelow = (xBelow + width) % width; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this actually wraps around? Is that really intended? I definitely didn't expect it. I would have thought it would map 0 to the first pixel and 1 to the last, instead both map to the average of first and last?
I'm not sure if wrapping is good default behaviour. If anything maybe we have to inject it, if we want to support it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Imagine a simple noise texture as actual pixels. The array here represents the pixels of that texture. The value you pass in is actually the continuous coordinate on that texture, and there's a good chance that coordinate doesn't exactly align with the centre of a pixel. This is where the interpolation method that gets passed in comes in. If you do bilinear interpolation, you need something to interpolate with. We could interpolate with 0, but this causes artefacts along the outside, which I think is undesirable.
Note that if you use nearest neighbour as interpolation method, this will all still add up to returning the value of pixel [0, 0] in coordinates (0, 0).
I'm not sure if wrapping is good default behaviour. If anything maybe we have to inject it, if we want to support it?
I agree the wrapping behaviour should be injected, but given the already existent complexity of this PR, has been deferred to future PRs. I think it makes sense to provide a default value though, and I chose repeat in this case (which is in line with the OpenGL texture default)
{ | ||
static class NoiseUtils | ||
{ | ||
internal static T[,] GenerateRandomArray<T>(int width, int height, Func<Random, T> generator, int? seed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of injecting a seed, I think it would be better to inject a Random instance. That would allow better control over where numbers come from and can prevent unnecessary allocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am happy to be convinced otherwise, but let me explain my reasoning:
One of the properties is that this method returns the exact same result if you give it the same input, i.e. it's deterministic (unless you choose not to pass in a seed). When you replace the seed with a Random
instance, this because much more nuanced. Sure, if you pass in the "same" Random
, it is still deterministic. However, there isn't an Equals
method on Random
, so the whole concept of having the "same" Random
is a bit weird. Using a seed makes this much more obviously correct.
Like I said, happy to be convinced otherwise.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean. In many other places we do inject Random though, like in the enumerable shuffle extensions, and we do the same in TD level generation. Would you argue we shouldn't do so there?
The main arguments I have is that injecting a seed is very low level (yuck, primitives), and that it causes potentially unnecessary allocation. Also, of course you could easily new Random(seed)
at the call site.
I think using Random is nicer for public interfaces at least.
.Select(vector => new Vector2d(vector.X, vector.Y)) | ||
.ToArray(); | ||
|
||
public static INoiseMap Generate(int numCellsX, int numCellsY, int? seed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not clear to me what the numCells parameters do/mean.
Next to that I think this should take a Random, not a seed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to know something about how Perlin noise works to understand these parameters. I have added a <summary>
fragment since I think that is the only way to explain how these parameters map to the underlying algorithm.
|
||
var gradientArray = NoiseUtils.GenerateRandomArray( | ||
// We generate the corners, but to make the noise map wrap along both dimensions, we reuse the same | ||
// values as the left and top boundaries, so we don't have to generate the right and bottom boundaries. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to above, why do we wannt it to wrap? Should that be the default? Should that be configurable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to above, why do we wannt it to wrap?
By default, yes.
Should that be the default?
Yes, this sounds the most expected and is in line with OpenGL defaults for traditional textures, and this doesn't immediately break cases that don't expect it.
Should that be configurable?
Yes, to be added in a future PR.
Still awaiting further review I believe. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's been a while since I last took a look, but I think I like it better now. :)
Bearded.Utilities.Tests/Core/interpolation/InterpolationMethod1Tests.cs
Outdated
Show resolved
Hide resolved
Bearded.Utilities.Tests/Core/interpolation/InterpolationMethod1Tests.cs
Outdated
Show resolved
Hide resolved
Bearded.Utilities.Tests/Core/interpolation/InterpolationMethod2Tests.cs
Outdated
Show resolved
Hide resolved
Ready for another round of review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you addressed my comments pretty well. Apart from what looks like a typo, go ahead.
Co-authored-by: Paul Scharf <[email protected]>
✨ What's this?
This PR introduces a new namespace that can be used to automatically generate noise. Specifically, we expose an interface that can return a value for a rectangular area based on an underlying noise function. This PR includes basic uniform noise and Perlin noise (the latter implementation pretty much straight up copied from TD).
🔍 Why do we want this?
Any algorithm generating 2D randomness (whether graphically or mechanically) will need functionality similar to the functionality in this PR. While uniform randomness is fairly simple to generate, Perlin randomness is a bit fiddly. Having reusable methods to "just" give you a Perlin noise map sounds hugely beneficial to anybody bootstrapping some of this generation in their game.
🏗 How is it done?
This PR describes an extendable set of interfaces for generating noise. The result of generating noise is an interface that allows the caller to look up a noise value for any coordinate within a rectangular area (upper bounds exclusive). This has already proven beneficial since Perlin noise and uniform noise have different methods of getting to a value based on their underlying noise array. It also allows for extendibility such as for example transformation methods, combining multiple noise maps in interesting ways, etc. without having to have to deal with a different interface. Of course performance concerns creep in at that point, but the current interface doesn't preclude you to "pre-calculate" a noise texture by sampling the underlying noise functions, though it is unlikely to give a huge performance boost overall unless you sample the same points a lot.
As explained above, the current PR introduces two noise types:
In addition, since the noise functionality had a need to pass in an interpolation method, I created a structure where interpolation methods have a very enum-like behaviour, but are in fact static objects under the hood. Introducing further interpolation methods should not be too hard, though further design will have to be done if we want to do something like (bi)cubic or Bezier interpolation, since they need additional values besides "from" and "to".
🔬 Why not another way?
There are a few decisions I made along the way. These are the options I remember rejecting:
🦋 Side effects
We may want to consider moving the existing
Interpolate
methods over, but since they are floats, and the new interpolation methods work with doubles, this would potentially be a bigger change.I can think of many additional improvements. I have created issues for all of them (I might remember more later): #222, #223, #224
💡 Review hints
I am aware this PR probably will not be without controversial choices, even if the actual added functionality is not ground-breaking. I considered splitting it already, but given GitHub's awkward support on stacking PRs and the fact that all of this goes together somewhat, I decided to keep it.
Feel free to do an initial pass over the proposed interfaces and APIs first before delving in implementations.
I would ask to focus review comments on the overall structure and decisions made here, rather than on the choice of what to include or not. As seen above, it was easy to come up with many future improvements to this framework, including potential performance optimisations. My hope with this PR is to have an initial foundation submitted.