An Island in the Clouds


Despite what the title of this post may suggest, I’m not going to be talking about some recent trip to Borneo or Papua New Guinea (although how cool would that be!) No, instead I’m talking about my continuing dalliance with the plasma fractal I mentioned in my last procedural content post.
While I used plasma fractals to create variations of the floor tiles in my procedural cave, there are many other applications for plasma fractals. One of the classic applications of plasma fractals is generating heightmaps. By imagining the value at each cell of a plasma fractal as the height of ground at that point, it’s relatively easy to visualize the mountainous terrain that can be generated with a plasma fractal. Traditionally, people use heightmaps values to perturb a 3D mesh grid to create 3D terrain. Since I already had the necessary pieces at my fingertips, I decided to see how my plasma fractals would look when rendered with 2D tiles.
To get a sense of how my heightmaps would look, my first step was to pass a plasma fractal into a class to render it as text. By mapping ranges of values to numbers as well as coloring the text according to those values, I was able to generate a visualization that would give me a sense of how my tiled heightmap would look.

I was now ready to render the heightmap with PlanetCute tiles. My basic approach was to vary the height of the tiles based on the values in the heightmap. I also darkened tiles based on their depth to provide a basic shading effect to the terrain.

It works, but using the same tile for the whole heightmap is dull. To add more variety to the heightmap, I modified the renderer to use different tiles based on where each heightmap value fell within a particular range. Declaring that values <= 0.3 map to be water tiles and >= 0.7 map to be dirt tiles gave me the following result.

Plasma fractals aren’t just useful for heightmaps, of course. In order to add a little more visual pizazz, I turned my eye to the sky and used a plasma fractal to generate a cloud-filled sky. By mapping the fractal’s values to the alpha channel of a white texture, I created a cloud texture that could turn this empty sky:

Into a cloud-filled one:

Also, by adjusting the alpha of the cloud texture, I could create a darker, stormy sky:

Of course, I kind of prefer the blue sky with white clouds. We have enough cloudy weather in games nowadays, don’t we?

For the interested, here’s the code for generating the heightmap and cloud textures:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace ProceduralWorldLib
{
    public class HeightfieldGenerator
    {
        public static float[] GenerateHeightfield(int randomSeed, int sideLength)       //Side length must be POW2
        {
            Random rng = new Random(randomSeed);
            float[] heightField;
            AlgorithmHelper.GeneratePlasma(out heightField, sideLength, rng, 0.5f, 0.75f);

            return heightField;
        }

        public static Texture2D GeneratePlasmaCloud(GraphicsDevice graphics, int randomSeed, int sideLength)
        {
            Random rng = new Random(randomSeed);
            float[] heightField;
            AlgorithmHelper.GeneratePlasma(out heightField, sideLength, rng, 0.5f, 0.75f);

            Texture2D sampleTexture = new Texture2D(graphics, sideLength, sideLength);
            Color[] colorData = new Color[sideLength * sideLength];
            for (int x = 0; x < sideLength; ++x)
            {
                for (int y = 0; y < sideLength; ++y)
                {
                    float genVal = Math.Min(Math.Max(heightField[sideLength * y + x], 0.0f), 1.0f);
                    colorData[y * sideLength + x] = new Color(genVal, genVal, genVal, genVal);
                }
            }
            sampleTexture.SetData<Color>(colorData);

            return sampleTexture;
        }
    }
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;

namespace ProceduralWorldLib
{
    public static class AlgorithmHelper
    {
        public static void GeneratePlasma(out float[] plasmaArray, int plasmaSideLength, Random rng, float plasmaSeedValue, float plasmaPerturbation)
        {
            plasmaArray = new float[(plasmaSideLength + 1) * (plasmaSideLength + 1)];

            plasmaArray[plasmaIndex(0, 0, plasmaSideLength)] = plasmaSeedValue;     //0,0
            plasmaArray[plasmaIndex(plasmaSideLength, 0, plasmaSideLength)] = plasmaSeedValue;     //MAX,0
            plasmaArray[plasmaIndex(0, plasmaSideLength, plasmaSideLength)] = plasmaSeedValue;     //0,MAX
            plasmaArray[plasmaIndex(plasmaSideLength, plasmaSideLength, plasmaSideLength)] = plasmaSeedValue;     //MAX,MAX

            float h = plasmaPerturbation;

            int currentSideLength = plasmaSideLength;
            while (currentSideLength >= 2)
            {
                int halfSideLength = currentSideLength / 2;
                for (int x = 0; x < plasmaSideLength; x += currentSideLength)
                {
                    for (int y = 0; y < plasmaSideLength; y += currentSideLength)
                    {
                        float average = plasmaArray[plasmaIndex(x, y, plasmaSideLength)]; //top left
                        average += plasmaArray[plasmaIndex(x + currentSideLength, y, plasmaSideLength)]; //top right
                        average += plasmaArray[plasmaIndex(x, y + currentSideLength, plasmaSideLength)]; //bottom left
                        average += plasmaArray[plasmaIndex(x + currentSideLength, y + currentSideLength, plasmaSideLength)]; //bottom right
                        average *= 0.25f;

                        plasmaArray[plasmaIndex(x + halfSideLength, y + halfSideLength, plasmaSideLength)] = average + (float)(rng.NextDouble() * 2.0) * h - h;
                    }
                }


                for (int x = 0; x < plasmaSideLength; x += halfSideLength)
                {
                    for (int y = (x + halfSideLength) % currentSideLength; y < plasmaSideLength; y += currentSideLength)
                    {
                        float average = plasmaArray[plasmaIndex((x - halfSideLength + (plasmaSideLength)) % (plasmaSideLength), y, plasmaSideLength)]; //left of center
                        average += plasmaArray[plasmaIndex((x + halfSideLength) % (plasmaSideLength), y, plasmaSideLength)]; //right of center
                        average += plasmaArray[plasmaIndex(x, (y + halfSideLength) % (plasmaSideLength), plasmaSideLength)]; //below center
                        average += plasmaArray[plasmaIndex(x, (y - halfSideLength + (plasmaSideLength)) % (plasmaSideLength), plasmaSideLength)]; //above center
                        average *= 0.25f;

                        plasmaArray[plasmaIndex(x, y, plasmaSideLength)] = average + (float)(rng.NextDouble() * 2.0) * h - h;

                        if (x == 0) plasmaArray[plasmaIndex(plasmaSideLength, y, plasmaSideLength)] = average;
                        if (y == 0) plasmaArray[plasmaIndex(x, plasmaSideLength, plasmaSideLength)] = average;
                    }
                }

                currentSideLength *= 0.5f;
                h *= 0.5f;
            }
        }

        static int plasmaIndex(int x, int y, int plasmaSideLength)
        {
            return (plasmaSideLength + 1) * y + x;
        }
    }
}
Share this Article:
  • Digg
  • StumbleUpon
  • del.icio.us
  • Facebook
  • Yahoo! Buzz
  • Twitter
  • Google Bookmarks
  • Print