Gamasutra is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.

A Real-Time Procedural Universe, Part One: Generating Planetary Bodies
June 20, 2021
View All     Post     RSS
June 20, 2021
Press Releases
June 20, 2021
Games Press

If you enjoy reading this site, you might also want to check out these UBM Tech sites:

A Real-Time Procedural Universe, Part One: Generating Planetary Bodies

March 2, 2001 Page 2 of 2

A Bit More Interesting: fBm
One of the most common complex functions to write using Perlin noise is called fractal Brownian motion, or fBm. Basic fBm is a fractal sum of the noise function which looks something like this:

noise(p) + 1/2 * noise(2 * p) + 1/4 * noise(4 * p) + ...

While reading this article, keep in mind that a number of information sources confuse fBm with Perlin noise. Any noise function can be used to compute a fractal sum. Perlin noise is just a fast method for generating high-quality noise.

To help you visualize what fBm output looks like, think of it as a weighted sum of the Perlin noise images shown above in Figure 1. It is usually implemented as a loop and the number of times to go through that loop is called the number of octaves. Each time through the loop, the coordinates passed to the noise function are scaled up by a certain factor and sent to the noise function, whose output is scaled down by a certain factor before adding it to the fractal sum. Because the noise function will return the same result for the same coordinates every time, you are essentially adding different parts of the same image to itself at different scales using different weights. A simple fBm routine looks a lot like this: A simple fBm routine is given in Listing 1.

LISTING 1. A simple fBm routine.

// The number of dimensions and the noise lattice are initialized
// separately in an Init() method. This code has been simplified
// for the article to make it easier to read.
float CFractal::fBm(float *f, int nOctaves)
{
float fValue = 0.0f;
float fWeight = 1.0f;
for(i=0; i
{
fValue += Noise(f) * fWeight; // Sum weighted noise value
fWeight *= 0.5f; // Adjust the weight
for(int j=0; j
f[j] *= 2.0;
}
return fValue;
}

 fBm (3 octaves) fBm (4 octaves) fBm (5 octaves) FIGURE 2. Samples of simple fBm using a different number of octaves.

It may be difficult to see at first, but blending the three images from Figure 1 gives us the first image in Figure 2. As more octaves are added, you can see how the basic pattern remains the same, but the pattern is perturbed at a finer level of detail with each octave. As with Perlin noise, you can zoom in or out on any part of these pictures by changing the range of numbers passed to the function. The farther you zoom in, the higher the number of octaves you need to maintain a good level of detail and complexity in the image.

LISTING 2. A more flexible fBm routine.

// Note the use of two extra member variables, the m_fExponent array
// and m_fLacunarity. Both are initialized in the Init() method, which
// allows you to customize the scaling factors used weight the noise
// values and to scale the coordinates. Also note that the number
// of octaves is now a float, allowing fractional parts of octaves.
float CFractal::fBm(float *f, float fOctaves)
{
float fValue = 0.0f;
for(i=0; i
{
fValue += Noise(f) * m_fExponent[i];// Sum weighted noise value
for(int j=0; j
f[j] *= m_fLacunarity;
}

// Take care of the fractional part of fOctaves
fOctaves -= (int)fOctaves;
if(fOctaves > 0.0f)
fValue += fOctaves * Noise(f) * m_fExponent[i];

return fValue;
}

 fBm (H = 0.9) fBm (H = 0.5) fBm (H = 0.1) FIGURE 3. Samples of simple fBm with different H values.

The exponent array that scales the result of the Noise() function is initialized using an exponential function that you control by changing a parameter called H, which acts as a roughness factor going from 0.0, which is very rough, to 1.0, which is very smooth. The difference in roughness is caused by how heavily the higher octaves are scaled, as the higher octaves contain much smaller details.

 fBm (lacunarity = 1.5) fBm (lacunarity = 2.0) fBm (lacunarity = 2.5) FIGURE 4. Samples of simple fBm with different lacunarity values.

Changing the lacunarity factor changes how your coordinates are scaled with each octave. It affects the output in odd ways, and I've read that most people just leave it at 2.0. Values between 1.0 and 2.0 seem to have some sort of recursive feedback, because you're getting close to blending an image with itself multiple times (think about the ranges you're passing into the noise function). Values below 1.0 actually make the noise ranges decrease with each octave, going from finer noise with a higher weight to coarser noise with a lower weight. Values above 2.0 cause your range to increase more quickly. In some ways that makes your image rougher because finer noise gets added with a higher weight, and in some ways it makes your image smoother because you more quickly get to a point where the noise is too fine to distinguish at the current resolution.

When using fBm-based algorithms to generate planetary bodies, keep in mind that the size of the planetary body, the range of numbers you pass in as coordinates, and the number of octaves you use work together to give you specific sizes of general terrain features (continents, ocean, coastlines, and so on) and the proper amount of detail given that range. The H and lacunarity factors also have a strong effect on your final output, especially when you zoom in. These are the kinds of things you just have to play around with for a while to get the feel of them.

The Next Step: Multi-fractals

The next level of noise-based algorithms has been called multi-fractals, and they're basically just a more complex form of fBm. Some perform a fractal product instead of a fractal sum (multiplying instead of adding). Some add variable offsets or apply other mathematical functions somewhere in the loop, like abs(), pow(), exp(), or some of the trig functions. Ken Musgrave has done a good bit of research in this area, and he's spent a lot of time working with multi-fractals to generate some interesting planetary models. There is a book he co-authored with Ken Perlin and some other big names in the graphics field called Texturing & Modeling: A Procedural Approach (Morgan Kaufmann, 1994). If you're interested in this subject, I strongly recommend that you pick up a copy. It goes into a lot more depth than I can fit into an article and covers a lot of other methods and uses for procedural algorithms.

I won't go over any specific examples of multi-fractals in this article except for the one I wrote to generate the planet in the demo. Like the fBm parameters, creating your own multi-fractal algorithm is just something you have to play around with and get a feel for. Keep in mind as you're testing things that some functions will look good on a planet from a distance, some will look good very close to the planet, some won't look good either way, and some will look good both ways. I feel that the one I wrote for the demo looks good both ways, and I'll explain the rationale behind what I did.

My planet function uses simple fBm, then takes the result and applies the power function to it. Since most of the numbers generated by simple fBm are between -1 and 1, this will tend to cause the numbers closer to 0 to flatten a bit. Thinking in terms of terrain, this causes land close to sea level to be more flat and land at higher altitudes to be more mountainous, which is somewhat realistic. Since this is not always the case on Earth, I call the noise function one more time to determine the exponent of the power function, which means you can sometimes have steeper land near sea level or flatter land at higher altitudes. For negative values, which indicate a value below sea level, the exponent is hard-coded to give a smoother ocean floor. (This may not be desirable if you want to have under-sea vessels such as submarines in your game.)

LISTING 3. The authors' planet function.

// The number of dimensions, the noise function, and the exponents are
// initialized in CFractal::Init(). To simplify the code for this
// article, the number of octaves is an integer and the function
// modifies the array of floats passed to it.
float CFractal::fBmTest(float *f, float fOctaves)
{
float fValue = 0.0f;
for(i=0; i
{
fValue += Noise(f) * m_fExponent[i];// Sum weighted noise value
for(int j=0; j
f[j] *= m_fLacunarity;
}

// Take care of the fractional part of fOctaves
fOctaves -= (int)fOctaves;
if(fOctaves > DELTA)
fValue += fOctaves * Noise(f) * m_fExponent[i];

if(fValue <= 0.0f)
return (float)-pow(-fValue, 0.7f);
return (float)pow(fValue, 1 + Noise(f) * fValue);
}

 Planet (from space) Planet (coastline) Planet (mountains) FIGURE 5. Sample images from the author's planet demo.

Don't get me wrong, it's not easy to figure out how a certain change to one of these algorithms will affect its output. If I had included a picture of its 2D output in grayscale, it would have looked a lot like the other fBm images I included, and there would have been no indication as to which was any better for generating planets. To get it just the way I wanted it, I had to tweak it a lot, looking at the planet close-up and from a distance using different initialization parameters. You should play around with it on your own for a while to get a good idea of how certain changes will affect your planet at different levels of detail.

I'm currently using 3D noise to generate my planet for the demo. When I want to create a new vertex at a certain position on the sphere, I pass it a normalized direction vector that points to the position of the vertex I want to create. I take the value returned, which should be close to the range of -1.0 to 1.0, and I scale it by the height I want my tallest mountain to be. Then I add that value to my planet's radius and multiply the unit vector by it to get a new vertex. All of these values are parameters I can use to initialize my planet object, along with the random seed, H factor, and lacunarity factor which affect the fBm output. This allows a wide range of planetary bodies to be created from one function.

The routine could be sped up using 2D noise and passing it latitude and longitude, but that would cause the terrain features to be compressed up near the poles, and a discontinuity would exist where the longitude wrapped around from 360 degrees to 0 degrees. Polar coordinates would not have compression at the poles, but would have two lines of discontinuity to worry about. If you try to skip a dimension, just passing X and Y for instance, you would end up with two hemispheres mirroring each other. If you can find a better way to represent 2D coordinates for a sphere that doesn't cause distortion, by all means try it to see how it looks and performs.

Final Notes

If you're interested, take a look at the source code for the demo. It uses OpenGL to handle all rendering, but it was written for Windows and doesn't currently compile under any other platforms. It shouldn't be very difficult to port it, but since my video card isn't supported very well under Linux, I never got around to it. The project was created with Microsoft Visual C++ 6.0, but it should compile without any problems using 5.0. Read the README.TXT file for the list of keyboard commands and some helpful tips.

Page 2 of 2

Related Jobs

Disbelief — Cambridge, Massachusetts, United States
[06.18.21]

Programmer
Disbelief — Cambridge, Massachusetts, United States
[06.18.21]

Senior Programmer
Insomniac Games — Burbank, California, United States
[06.18.21]

Technical Artist - Pipeline
Insomniac Games — Burbank, California, United States
[06.18.21]

Technical Artist - Pipeline