A Real-Time Procedural Universe, Part One: Generating Planetary
Bodies
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<nOctaves;
i++)
{
fValue
+= Noise(f) * fWeight; // Sum weighted noise
value
fWeight *= 0.5f; // Adjust
the weight
for(int j=0;
j<m_nDimensions; j++) // Scale the
coordinates
f
*= 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<fOctaves;
i++)
{
fValue
+= Noise(f) * m_fExponent;// Sum weighted noise
value
for(int j=0;
j<m_nDimensions; j++) // Scale the
coordinates
f*=
m_fLacunarity;
}
//
Take care of the fractional part of fOctaves
fOctaves
-= (int)fOctaves;
if(fOctaves >
0.0f)
fValue += fOctaves * Noise(f) *
m_fExponent;
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<fOctaves;
i++)
{
fValue
+= Noise(f) * m_fExponent;// Sum weighted noise
value
for(int j=0;
j<m_nDimensions; j++) // Scale the
coordinates
f
*= m_fLacunarity;
}
//
Take care of the fractional part of fOctaves
fOctaves
-= (int)fOctaves;
if(fOctaves >
DELTA)
fValue += fOctaves *
Noise(f) * m_fExponent;
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.
Discuss this article in Gamasutra's discussion forum