Infinite terrain generation in chunks - need hints on handling chunk edges

Started by
33 comments, last by Kylotan 11 years, 1 month ago
note that the sample-count is usually bounded and wraps, but applies over a potentially large area, and is not used up all at once.

going outside of the sample range, as can be noted, tends to result in repeating patterns.
a partial trick here is the use of a per-region noise function (in my case, a region is 512x512x128m), and interpolating over the region-edges via a windowing function. then a hash-function or similar can be used to calculate a per-region seed.

the idea would basically to multiply the values by a sin or sin^2 curve, where as the curve for one side is falling off, the curve for the other side is rising, say to ideally create a case where f(theta0)+f(theta1)=1, where the theta values will depend on the positions within the region.

except for potentially very large/low-frequency features (such as biomes), this works ok, but then potentially a person can use a few global noise functions (the world probably wont be big enough for this to matter).
Advertisement

You can't generate an infinite non-repeating terrain from a fixed set of samples - entropy's a bitch.

Your choices are to loop the output (fine for fairly small worlds), or generate additional samples once you are far enough away from the origin.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Swiftcoder: I don't want to use a fixed set of samples. Not sure where it was implied that I did? The typical Perlin noise algorithms do use a fixed set, which I'm trying to do away with by using a hash function instead (as mentioned in my first post), but I'm not sure of the implications for what values I need to pass in for each of the different 'octaves'.

On reflection it might be that I'm thinking too hard about the usual implementation and forgetting that I just need to add together several continuous functions. It's just confusing because I've seen the 'frequency' parameter defined in terms of the original pregenerated noise sample map, and obviously that doesn't work. eg. The usual site: http://freespace.virgin.net/hugo.elias/models/m_perlin.htm - has frequency as basically 'how many samples to take from the whole set of noise' or 'how many samples to take across the whole area being generated', both of which are generally equal in the usual implementation, but which doesn't apply in my case.

I guess I need to think of it as a different type of value, as I don't think frequency itself has any meaning in my situation.

cr88192: I must admit I have no idea how any of that would work - although I'm not sure I would need it if I had a function that was defined for all possible values of x any y. I think "interpolating over the region-edges via a windowing function" is essentially what I described originally, bilinearly interpolating between the values for adjacent regions - right?

The usual site: http://freespace.virgin.net/hugo.elias/models/m_perlin.htm

As I've been saying for years, that site ought to die in a fire. It contains more terminology confusion than it does useful information...

Yes, throw out pre-conceived notions of how "standard" noise generators are implemented. You just need to hash on some sort grid, and generate noise based on that.

For example, I have an infinite voronoi cell noise generator. It divides the world into a square grid at some input-defined resolution, and in each cell it generates a fixed number of points. The resulting noise is a function of the distance to the points in the 3x3 grid of cells surrounding the input coordinate.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Detailed generation of "infinite" worlds is a bitch. I've put myself through it and learned much but, currently am not applying everything I've learned because I'd have to bring my entire project back to the drawing board. It's a good thing you're asking because you really want to get this right early on.

First off, a noise function will not cut it. Sampling a point at (x, y) from a noise function to get a height value will produce very boring results. Having an infinite world is pretty pointless if it's all homogeneous. You will need to implement different methods of creation. Noise (spectral synthesis) is always useful. Displacement (also known as distortion or perturbation) really helps to produce natural results. The use of convex hull diagrams, such as Voronoi Diagrams, are very important. The ability to use modifiers on output, from basic mathematical operations to more complex diagram conversion algorithms, will open you up to more flexibility. All of this is hell on seems though. Ex: displacement can really put the edges of a terrain patch far out of whack with it's neighbors.

Second, how you generate terrain, handle collision detection of terrain and render terrain should probably not be the same. It's efficient to generate large patches of terrain but more efficient to render in smaller patches, especially for dynamic detailing. Collision detection depends on the library you are using but I try to get everything in place as it's generated. I say it's efficient to generate larger patches because you want to leave yourself plenty of resources for dealing with seems. The smaller the patch, the more seems that have to be managed.

I would suggest basing the generation of the terrain on both elevation and vertex normals. Generating patches separately will always leave you with visible seems, even if the elevations match. For example, the resulting edge of one patch may be horizontal while the edge of it's neighbor rises sharply, creating a highly visible "L" shape along the seem. So, rather than using something like midpoint averages, include the vertex normal in your calculations to keep a more consistent surface (ex: a surface of an elevation can "lean" away from the midpoint, resulting in a higher midpoint and smoother surface). If a surface at one edge is horizontal (or anything else), the vertex normal insures that it's neighbor is the same.

Another solution would be to generate "areas" or "continents" before generating actual terrain. The area can be defined by varying dimensions, by a particular method(s) of generation and produce a terrain with a central high point and lower-lying edges. This could be overlapped by other areas, only the higher values of the two areas being used and everything else discarded.

There's a lot to this and I've taken tons of notes (much of it in exasperation). Dealing with texture generation, shadowing, collision detection and, likely, multi-threading, can be a real nightmare. Once I'm done with work, I'll try to go over my notes and get back to this thread.

Quit screwin' around! - Brock Samson

I appreciate the mention of Voronoi methods and the like but all that is out of my scope - height-mapped noise is sufficient for my purposes. I may not have made it clear but I can't generate a whole world at any point - the idea is to be able to procedurally generate individual chunks as and when they're required, which requires a continuous function that I can sample at any point in the future to get results from.

I'm also glad that the link I cited is considered to have problems with it because that helps to explain why I've had trouble reconciling my idea of what the terms mean and my idea of how the algorithm would work.

I'm still slightly unclear on how to implement my algorithm however, given that every single resource I'm finding has some terminology issue or a fixed-size assumption. Given an input value 'x', how do I derive a height 'y', given an arbitrary number of 'octaves', ie. coherent noise functions with changing amplitude and frequency in inverse proportions? I figure that I need two adjacent hash values and the frequency value for the current octave determines an interpolated point between the two values - but I have been staring at this all day and really can't work out how to implement it properly. I'm sure it's a trivial one or two liner but I don't know how to do it.

Given some arbitrary noise function Noise(x, y), a fairly vanilla fractal function might look like this:
generate(x, y) {
	frequency = 1.0
	amplitude = 1.0
	octaves = 8
	
	result = 0.0
	
	for i from 0 to octaves {
		result += Noise(x*frequency, y*frequency) * amplitude

		frecuency *= 2.0
		amplitude *= 0.5
	}
	
	return result
}
Where octaves is the number of layers to combine, frequency is the frequency at which to sample the noise function, and amplitude is the magnitude of each layer.

I'm not seeing any dependency on world size here (at least until you run out of floating point precision). As long as your Noise(x, y) is continuous, your world can be any size, and you can generate it in whatever increments you desire.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

But I don't see that would work for an arbitrary noise function. Imagine the noise function is based around a hash function that takes an integer in and yields a float from 0-1 out. The only way I'd ever get any smoothing is if I pass in fractional values of X which force it to interpolate between two adjacent hash results. But in this example, the frequency is always a positive integer. So if I sample at x=1, x=2,... x=123456778, I'm always going to be handling whole numbers in the noise function and getting arbitrary non-interpolated numbers back out, making the whole coherent noise thing pointless.

It seems to me like I want to be dividing the input parameters by frequency rather than multiplying by it? That makes it "the frequency with which we sample between 2 points", which makes sense on some level. But nobody seems to be doing that, hence my (continued) confusion.

Or alternatively, the noise function doesn't expect to have random numbers exactly on the whole-number boundaries - but then I'm not sure how to construct it in a reasonable way.

The point of something like fBm (which is roughly the function I reproduced above), is that successive octaves display increased detail - each layer decreases in both scale and amplitude (scale being the inverse of frequency).

Think of it this way: if you view your noise basis as a 2-dimensional image, then increasing the frequency is equivalent to zooming out the image. When you zoom out the image, visible features become smaller, which increases the visible detail.

If we go your route, and take it as the "frequency with which we sample between 2 points", then your noise basis needs to have infinite levels of detail from the get go (at which point you might as well just sample it once; it wouldn't need multiple octaves to build up fine detail on top of a coarse base).

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

I understand the principle but the implementation isn't working. I know the idea is to increase the detail at higher octaves; but as I see it, the detail is going to be at maximum right from the start if I'm sampling at integer boundaries (which I will - because ultimately I need to pass this data to a system that needs discrete values).

Example:


float hash(x) { return [0.56, 0.1, 0.99, 0.44, 0.63, 0.72, 0.1, 0.72, 0.01, 0.2]; } // pretend these are pseudo-random
float Noise1D(x) { return Interpolate(hash(x), hash(x+1), FractionalPartOf(x); }

Now if I call Noise1D with the numbers 0 and 1 consecutively, I get exactly 0.56 and 0.1 out.

If I double the frequency and call it with the numbers 0 and 1, I get exactly 0.56 and 0.99 out.

If I double it again and call it again, the results are 0.56 and 0.63 out.

None of these values will ever hit the interpolation part as FractionalPartOf(x) always returns 0. In this case the Noise function is no different from a discrete random number generator, and no smoothing is being performed. The detail is effectively at maximum all the time, for any whole-number value for frequency.

I need a system where I can call Noise(1) and Noise(2) and get 2 values that have some degree of coherence. Either this requires a rethinking of what exactly the 'frequency' is or I need a better definition of what Noise(x) is meant to look like. I can't see how the frequency value can be premultiplied with X and Y because surely the implementation of Noise needs both the X,Y and the frequency information to find 2 samples to interpolate from?

This topic is closed to new replies.

Advertisement