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 highquality
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 fBmbased 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:
Multifractals
The
next level of noisebased algorithms has been called multifractals,
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 multifractals to generate some interesting planetary
models. There is a book he coauthored 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 multifractals in this article except
for the one I wrote to generate the planet in the demo. Like the fBm
parameters, creating your own multifractal 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 hardcoded to give a smoother ocean floor. (This may
not be desirable if you want to have undersea 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 closeup 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.