Example of Procedural Planet generation?

Hi,

Does any one have share-able example of “Procedural Planet” generation script (pref. C#) or anything explains how game makers simulate “endless” space environment?

I want to toy with this technique to learn something new.

Thanks :slight_smile:

Last edit for this, but it should be placed at the top… someone has done what I was suggesting here, and when you purchase, they give you the source:

well… lets see… i have done this sort of thinng before…

i used fbm for generating height map on the fly, used spherical harmonics to get the map to look correct at the poles, and used geoclipmapping to dothe planet… now, when i added streets and such i used l-systems.

details… i generated several textures for the planets, 1 for height, 1 for diffuse, 1 for normals, then some via masks for roads…
geoclipmapping, i tried to do spherical geo clipmapping, but that was a bit over my head and just couldn’t get it to work. what this does is take a center point that is under your camera and make it maximum detail, then at a specific distance you have another level that is less detail which merges with the points from full detail section. you do this for 5 or so levels each with progressively less detail until you hit a flat surface. now the terrain data is stored in a toroidal array, which means you move all the verts / faces, NOT the camera (called camera space).

now, to tell you the truth… it is a LOT of work getting this done… google the specific words there and it will get you on your way :). also to note, i didn’t do this in unity. if you have a hard time finding the stuff i am talking about, write back here and i will give you the links (i am on ipad and too lazy to get all the details for you :wink: )

Edit (Not on the iPad anymore and not as lazy):
the first one is on clipmaps, and this is a very good resource on how to do them. He talks about a few different kinds of clipmaps, and how to do them on the GPU

(Proof of concept here, it is possible in unity: Unity3D Geoclipmapping terrain - YouTube)

Now for Fractal Brownian Motion (think of it as an advanced version of perlin noise), this link is quite math intensive, but isn’t as hard as it looks. The think about fbm is that it has octaves, which gives the impression that you are zooming in.
http://harrisd.net/papers/Id/MandelbrotVanNess1968SIAM.pdf
(this is fbm implemented)
http://www.allegro.cc/forums/thread/604600

Now for l-systems… These are actually quite easy to understand as well. The tree tool in unity already uses this type of system :slight_smile:
http://www.cs.purdue.edu/homes/aliaga/cs197-10/papers/p_cities.pdf

crap, had to edit again, forgot the spherical harmonics part:
Spherical harmonics is used to bend textures at the poles to prevent a texture artifact that comes with texturing a sphere
(http://jwhigham.files.wordpress.com/2010/05/uniformcloudsplanet.jpg)

as you can see, this is sort of a square peg in a round hole sort of problem… You need to spherize your square texture to make it work. There are a few different methods to make this work, like the one used to fix the picture above:

The problem you have is that you need to do this on the fly, as you are generating planets on the fly using a seed (I assume you are going for a galaxy, correct?)
here is the math for you:

now, I will be the first to admit, this took me a long while to understand, so here is a start on a cpu based implementation of the perlin noise + SH (can’t give you everything, else you wouldn’t learn :D) The code below was done in XNA, but the theory is the same no matter which engine you use

/*  --------------------------------------------------------------------------------------------
 *  -   perlinPlanet
 *  --------------------------------------------------------------------------------------------
 *  Built by: Uncasid
 *  Last Updated: 10/13/10 4:00pm MST
 *  Description:
 *   This class is used to build a planet based on perlin noise.
 *   
 *  Requirements:
 *   + Content (mesh) for sphere
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace Planet
{
    class cpuPerlinPlanet : GameObjects._Prototypes.aBaseRenderableWorldObject
    {
        #region Fields
        //Old planet
        //private Primitives.cPrimSphere PlanetMesh;
        private Model PlanetMesh;
        private Effect shader;
        private Effect shaderPerlin;
        private Effect DepthAndNormalsEffect;
        //private Texture2D PlanetTex;
        private Texture2D PlanetNrmTex;
        private Texture2D PlanetSpecTex;
        private Texture2D CloudTex;
        private Texture2D gTexture;
        private cAtmosphere Atmosphere;

        private float PlanetSize;
        //private DepthStencilBuffer tmpBuffer;
        //private DepthStencilBuffer cachedBuffer;

        private Controllers.Lights.LightTypes.genericLight _Light;

        #endregion

        private int Seed;

        //Texture2D heightTexture_;
        //Texture2D colorTexture_;
        Texture2D normalTexture_;

        Texture2D WaterTex, GrassTex, RockTex, SnowTex, WaterNormTex, GrassNormTex, RockNormTex, SnowNormTex;

        Texture2D AlphaMap;

        const int MapShift = 11;
        const int MapSize = (1 << MapShift);
        const int MapAnd = (MapSize - 1);

        const float SeaThreshold = 0.8f;
        const float HillsThreshold = 0.85f;
        const float MountainThreshold = 0.9f;
        const float SnowThreshold = 0.999f;
        const float NormalMapStrength = 4.0f;

        public float rotationSpeedX { get; set; }
        public float rotationSpeedY { get; set; }
        public float rotationSpeedZ { get; set; }

        /// <summary>
        /// Constructs a new Planet,
        /// with the specified size and tessellation level.
        /// </summary>
        public cpuPerlinPlanet(int seed, Controllers.Lights.LightTypes.genericLight Light, float diameter, cAtmosphere aAtmosphere)
            : base()
        {

            //, Game game, GraphicsDevice GraphicsDevice
            Seed = seed;
            //_game = game;
            PlanetSize = diameter;
            Atmosphere = aAtmosphere;
            //_GraphicsDevice = GraphicsDevice;
            _Light = Light;

            position = new MathLib.Vector3_64(0d, 0d, 0d);
            rotation = new Quaternion(0, 0, 0, 1);
            scale = new Vector3(diameter, diameter, diameter);
        }

        /// <summary>
        /// Load the content
        /// </summary>
        public override void LoadContent()
        {
            // Create the perlin texture for the planet
            GenerateTextures(Seed);

            shader = GameEngine.mContentManager.Load<Effect>("Content/Shaders/Mesh/PlanetShader");
            DepthAndNormalsEffect = GameEngine.mContentManager.Load<Effect>("Content/Shaders/Mesh/PlanetNormalsDepth");

            //PlanetTex = colorTexture_;
            PlanetNrmTex = normalTexture_;
            PlanetSpecTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/earthmap10k_SPEC");

            WaterTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthWater");
            GrassTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthGrass");
            RockTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthRock");
            SnowTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthSnow");

            WaterNormTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthWater_NRM");
            GrassNormTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthGrass_NRM");
            RockNormTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthRock_NRM");
            SnowNormTex = GameEngine.mContentManager.Load<Texture2D>("Content/Textures/Planet/Generic/EarthSnow_NRM");

            PlanetMesh = GameEngine.mContentManager.Load<Model>("Content/Models/Planet50k");

            CloudTex = Atmosphere.CloudTex;
            gTexture = Atmosphere.texture;

            // Set the depth buffer
            //this.cachedBuffer = new DepthStencilBuffer(shader.GraphicsDevice, Core.getInstance().GraphicsDevice.Viewport.Width, Core.getInstance().GraphicsDevice.Viewport.Height, DepthFormat.Depth16);
        }

        /// <summary>
        /// Generate the textures for the perlin planet
        /// </summary>
        /// <param name="seed"></param>
        private void GenerateTextures(int seed)
        {
            Random rand = new Random(seed);

            //  generate a height map
            float[] h = new float[MapSize * MapSize];
            //  use midpoint displacement in a breadth-first manner
            float distance = 1.0f;
            int countdown = 2;
            for (int generation = 0; generation <= MapShift; ++generation)
            {
                //  for each generation, perturb the lower-right corner of successively 
                //  smaller divided squares
                int add = (1 << (MapShift - generation));
                for (int y = add; y <= MapSize; y += add)
                {
                    for (int x = add; x <= MapSize; x += add)
                    {
                        h[((y & MapAnd) * MapSize) + (x & MapAnd)] += (generation == 0) ? 0.5f : ((float)(rand.NextDouble() - 0.5) * distance);
                    }
                }
                if (generation < MapShift)
                {
                    System.Diagnostics.Debug.Assert(add > 1);
                    int off = add >> 1;
                    //  average the edges, and the midpoint of the square
                    for (int y = 0; y < MapSize; y += add)
                    {
                        for (int x = 0; x < MapSize; x += add)
                        {
                            float h00 = h[((y & MapAnd) * MapSize) + (x & MapAnd)];
                            float h01 = h[((y & MapAnd) * MapSize) + ((x + add) & MapAnd)];
                            float h10 = h[(((y + add) & MapAnd) * MapSize) + (x & MapAnd)];
                            float h11 = h[(((y + add) & MapAnd) * MapSize) + ((x + add) & MapAnd)];
                            h[((y & MapAnd) * MapSize) + ((x + off) & MapAnd)] = (h00 + h01) * 0.5f;
                            h[(((y + off) & MapAnd) * MapSize) + (x & MapAnd)] = (h00 + h10) * 0.5f;
                            h[(((y + add) & MapAnd) * MapSize) + ((x + off) & MapAnd)] = (h10 + h11) * 0.5f;
                            h[(((y + off) & MapAnd) * MapSize) + ((x + add) & MapAnd)] = (h01 + h11) * 0.5f;
                            h[(((y + off) & MapAnd) * MapSize) + ((x + off) & MapAnd)] = (h00 + h01 + h10 + h11) * 0.25f;
                        }
                    }
                }
                if (countdown > 0)
                    --countdown;
                else
                    distance = distance * 0.5f;
            }

            //  now, filter the edges of the texture because the poles are smaller area than the equator
            for (int i = 0; i < MapSize / 4; ++i)
            {
                float f = 0;
                int bins = i;
                float weight = 1 - i / (MapSize / 4);
                int curbin = 0;
                int curcnt = 0;
                int binoffset = 0;
                int off = i * MapSize;
                for (int j = 0; j < MapSize; ++j)
                {
                    int bin = j * bins / MapSize;
                    if (bin != curbin)
                    {
                        f /= curcnt;
                        for (int q = binoffset; q < j; ++q)
                            h[off + q] = h[off + q] * (1 - weight) + f * weight;
                        f = 0;
                        curcnt = 0;
                        curbin = bin;
                        binoffset = j;
                    }
                    f += h[off + j];
                    ++curcnt;
                }
                f /= curcnt;
                for (int q = binoffset; q < MapSize; ++q)
                    h[off + q] = h[off + q] * (1 - weight) + f * weight;
                off = (MapSize - i - 1) * MapSize;
                f = 0;
                curbin = 0;
                curcnt = 0;
                binoffset = 0;
                for (int j = 0; j < MapSize; ++j)
                {
                    int bin = j * bins / MapSize;
                    if (bin != curbin)
                    {
                        f /= curcnt;
                        for (int q = binoffset; q < j; ++q)
                            h[off + q] = h[off + q] * (1 - weight) + f * weight;
                        f = 0;
                        curcnt = 0;
                        curbin = bin;
                        binoffset = j;
                    }
                    f += h[off + j];
                    ++curcnt;
                }
                f /= curcnt;
                for (int q = binoffset; q < MapSize; ++q)
                    h[off + q] = h[off + q] * (1 - weight) + f * weight;
            }

            Color[] c = new Color[MapSize * MapSize];
            Color[] c2 = new Color[MapSize * MapSize];
            
            for (int y = 0; y < MapSize; ++y)
                for (int x = 0; x < MapSize; ++x)
                {
                    float height = h[y * MapSize + x];

                    if (height < SeaThreshold)
                        c2[y * MapSize + x].R = 255;
                    else c2[y * MapSize + x].R = 0;

                    if (height >= SeaThreshold && height < HillsThreshold)
                        c2[y * MapSize + x].G = 255;
                    else c2[y * MapSize + x].G = 0;

                    if (height >= HillsThreshold && height < MountainThreshold)
                        c2[y * MapSize + x].B = 255;
                    else c2[y * MapSize + x].B = 0;

                    if (height >= SnowThreshold)
                        c2[y * MapSize + x].A = 255;
                    else c2[y * MapSize + x].A = 0;
                    c[y * MapSize + x] = HeightToColor(h[y * MapSize + x], y);
                }
            
            //if (colorTexture_ != null)
            //    colorTexture_.Dispose();
            
            //colorTexture_ = new Texture2D(_game.GraphicsDevice, MapSize, MapSize, 0, TextureUsage.AutoGenerateMipMap, SurfaceFormat.Color);
            //colorTexture_.SetData<Color>(c);

            if (AlphaMap != null)
                AlphaMap.Dispose();

            AlphaMap = new Texture2D(GameEngine.mGraphicsDevice, MapSize, MapSize, false, SurfaceFormat.Color);
            AlphaMap.SetData<Color>(c2);

            for (int y = 0; y < MapSize; ++y)
                for (int x = 0; x < MapSize; ++x)
                    c[y * MapSize + x] = CalcNormal(ref h, x, y);
            
            if (normalTexture_ != null)
                normalTexture_.Dispose();

            normalTexture_ = new Texture2D(GameEngine.mGraphicsDevice, MapSize, MapSize, false, SurfaceFormat.Color);
            
            normalTexture_.SetData<Color>(c);

            PostprocessPipeline.PostProcessComponent Blur = new PostprocessPipeline.GaussBlurComponent(_Light);
            Blur.LoadContent();
            Blur.Draw(new GameTime());
            AlphaMap = Blur.outputRT.RenderTarget;
        }

        /// <summary>
        /// Calculate the normals
        /// </summary>
        /// <param name="h">Height data</param>
        /// <param name="x">X position</param>
        /// <param name="y">Y position</param>
        /// <returns></returns>
        private Color CalcNormal(ref float[] h, int x, int y)
        {
            float du = (h[x + y * MapSize] - h[((x + 1) & MapAnd) + y * MapSize]) * NormalMapStrength;
            float dv = (h[x + y * MapSize] - h[x + ((y + 1) & MapAnd) * MapSize]) * NormalMapStrength;
            float z = (float)Math.Abs(du) + Math.Abs(dv);
            if (z > 1)
            {
                du /= z;
                dv /= z;
            }
            float dw = (float)Math.Sqrt(1.0f - du * du - dv * dv);
            return new Color(new Vector3(du * 0.5f + 0.5f, dv * 0.5f + 0.5f, dw));
        }

        /// <summary>
        /// Create a color based on the height
        /// </summary>
        /// <param name="h"></param>
        /// <param name="latitude"></param>
        /// <returns>Color</returns>
        private Color HeightToColor(float h, int latitude)
        {
            float latScale = (100.0f + Math.Abs(latitude - MapSize / 2)) / 200.0f;
            float w;
            if (h < SeaThreshold)
            {
                w = SeaThreshold - h;
                if (w > 1) w = 1;
                return Color.Lerp(Color.Blue, Color.DarkBlue, w);
            }
            //  make things colder outside of the equator
            h = SeaThreshold + (h - SeaThreshold) * latScale;
            if (h < MountainThreshold)
            {
                w = (MountainThreshold - h) / (MountainThreshold - SeaThreshold);
                return Color.Lerp(Color.DarkGreen, Color.DarkKhaki, w);
            }
            if (h < SnowThreshold)
            {
                w = (SnowThreshold - h) / (SnowThreshold - MountainThreshold);
                return Color.Lerp(Color.LightGray, Color.DarkGray, w);
            }
            w = h - SnowThreshold;
            if (w > 0.5f) w = 0.5f;
            return Color.Lerp(Color.LightCyan, Color.White, w + 0.5f);
        }

        /// <summary>
        /// Update the rotation of the planet
        /// </summary>
        /// <param name="cameraPosition"></param>
        /// <param name="gameTime"></param>
        internal override void Update(GameTime gameTime)
        {
            Vector3 axisX = new Vector3(1, 0, 0);
            Vector3 axisY = new Vector3(0, 1, 0);
            Vector3 axisZ = new Vector3(0, 0, 1);
            
            axisX = Vector3.Transform(axisX, Matrix.CreateFromQuaternion(this.rotation));
            axisY = Vector3.Transform(axisY, Matrix.CreateFromQuaternion(this.rotation));
            axisY = Vector3.Transform(axisZ, Matrix.CreateFromQuaternion(this.rotation));

            this.rotation = Quaternion.Normalize(Quaternion.CreateFromAxisAngle(axisX, this.rotationSpeedX * gameTime.ElapsedGameTime.Milliseconds) * Quaternion.CreateFromAxisAngle(axisY, this.rotationSpeedY * gameTime.ElapsedGameTime.Milliseconds) * Quaternion.CreateFromAxisAngle(axisZ, this.rotationSpeedZ * gameTime.ElapsedGameTime.Milliseconds) * this.rotation);

            base.Update(gameTime);
        }

        /// <summary>
        /// Draws the planet, using the specified effect. Unlike the other
        /// Draw overload where you just specify the world/view/projection matrices
        /// and color, this method does not set any renderstates, so you must make
        /// sure all states are set to sensible values before you call it.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            // Set important renderstates.
            //RenderState renderState = shader.GraphicsDevice.RenderState;
            //renderState.AlphaTestEnable = false;
            //Set renderstates for opaque rendering.
            //renderState.AlphaBlendEnable = false;
            //renderState.CullMode = CullMode.CullCounterClockwiseFace;

            // Updated renderstates for stencil buffer
            //renderState.DepthBufferEnable = true;
            //renderState.DepthBufferFunction = CompareFunction.LessEqual;
            //renderState.DepthBufferWriteEnable = true;

            // Cache the current depth buffer  
            //this.tmpBuffer = shader.GraphicsDevice.DepthStencilBuffer;

            //shader.GraphicsDevice.DepthStencilBuffer = this.cachedBuffer; //new DepthStencilBuffer(shader.GraphicsDevice, Core.getInstance().GraphicsDevice.Viewport.Width, Core.getInstance().GraphicsDevice.Viewport.Height, DepthFormat.Depth16);
            //shader.GraphicsDevice.DepthStencilBuffer = new DepthStencilBuffer(shader.GraphicsDevice, Core.getInstance().GraphicsDevice.Viewport.Width, Core.getInstance().GraphicsDevice.Viewport.Height, DepthFormat.Depth15Stencil1);  
            
            // Set our custom depth buffer  
            //shader.GraphicsDevice.DepthStencilBuffer = shadowDepthBuffer;  

            shader.Parameters["World"].SetValue(this.myWorldMatrix);
            shader.Parameters["WorldIT"].SetValue(this.myWorldInverseTranspose);
            shader.Parameters["View"].SetValue(GameEngine.mCameraManager.activeCamera.myViewMatrix);
            shader.Parameters["Projection"].SetValue(GameEngine.mCameraManager.activeCamera.myProjectionMatrix);
            //shader.Parameters["PlanetTex"].SetValue(PlanetTex);
            shader.Parameters["Splat1"].SetValue(WaterTex);
            shader.Parameters["Splat2"].SetValue(GrassTex);
            shader.Parameters["Splat3"].SetValue(RockTex);
            shader.Parameters["Splat4"].SetValue(SnowTex);
            shader.Parameters["AlphaMap"].SetValue(AlphaMap);
            shader.Parameters["CloudDiffuse"].SetValue(CloudTex);
            shader.Parameters["gTex"].SetValue(gTexture);
            shader.Parameters["ReflectionMap"].SetValue(PlanetSpecTex);
            shader.Parameters["AtmosphereHeight"].SetValue(Atmosphere.AtmosphereSize);
            shader.Parameters["elaspedtime"].SetValue((float)gameTime.TotalGameTime.TotalSeconds * 3);
            shader.Parameters["LightPos"].SetValue(_Light.position_32);
            shader.Parameters["FarPlane"].SetValue(GameEngine.mCameraManager.activeCamera.maximumDepth);

            Texture2D LightBuffer = LightPrePass.Manager.GetLightBuffer();
            shader.Parameters["LightBuffer"].SetValue(LightBuffer);

            Vector2 HalfPixel = new Vector2
            (
                0.5f / (float)LightPrePass.Manager.LightingBuffer.ScreenSize.X,
                0.5f / (float)LightPrePass.Manager.LightingBuffer.ScreenSize.Y
            );
            shader.Parameters["HalfPixel"].SetValue(HalfPixel);

            // Draw the model. A model can have multiple meshes, so loop.
            foreach (ModelMesh mesh in PlanetMesh.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    part.Effect = shader;
                }

                // Draw the mesh, using the effect set above.
                mesh.Draw();
            }

            base.Draw(gameTime);

            // Set the buffer back to it's original
            //shader.GraphicsDevice.DepthStencilBuffer = tmpBuffer;
        }

        /// <summary>
        /// Draw the planet
        /// </summary>
        /// <param name="gameTime"></param>
        /// <param name="effect"></param>
        /// <param name="WorldViewProjection"></param>
        public override void Draw(GameTime gameTime, Effect effect, Matrix WorldViewProjection)
        {
            effect.Parameters["WorldViewProj"].SetValue(WorldViewProjection);

            // Draw the model. A model can have multiple meshes, so loop.
            foreach (ModelMesh mesh in PlanetMesh.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    part.Effect = effect;
                }

                // Draw the mesh, using the effect set above.
                mesh.Draw();
            }

            base.Draw(gameTime);
        }

        public void DrawNormalsAndDepth()
        {
            DepthAndNormalsEffect.Parameters["World"].SetValue(myWorldMatrix);
            DepthAndNormalsEffect.Parameters["WorldIT"].SetValue(this.myWorldInverseTranspose);
            DepthAndNormalsEffect.Parameters["View"].SetValue(GameEngine.mCameraManager.activeCamera.myViewMatrix);
            DepthAndNormalsEffect.Parameters["Projection"].SetValue(GameEngine.mCameraManager.activeCamera.myProjectionMatrix);
            DepthAndNormalsEffect.Parameters["FarPlane"].SetValue(GameEngine.mCameraManager.activeCamera.maximumDepth);
            DepthAndNormalsEffect.Parameters["Splat1Norm"].SetValue(WaterNormTex);
            DepthAndNormalsEffect.Parameters["Splat2Norm"].SetValue(GrassNormTex);
            DepthAndNormalsEffect.Parameters["Splat3Norm"].SetValue(RockNormTex);
            DepthAndNormalsEffect.Parameters["Splat4Norm"].SetValue(SnowNormTex);
            DepthAndNormalsEffect.Parameters["AlphaMap"].SetValue(AlphaMap);

            // Draw the model. A model can have multiple meshes, so loop.
            foreach (ModelMesh mesh in PlanetMesh.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    part.Effect = DepthAndNormalsEffect;
                }

                // Draw the mesh, using the effect set above.
                mesh.Draw();
            }
        }
    }
}

Hopefully this is more useful for you, and good luck!

With sources: